diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 55235893b..1edb42b25 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -13,6 +13,16 @@ https://docs.djangoproject.com/en/1.11/ref/settings/ import os from django.utils.translation import gettext_lazy as _ +# Patreon OAuth Configuration for Paid Plugins +# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only. +# For repository version, use empty defaults and set via environment variables. +PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7') +PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv') +PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '') +PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier +PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI') +PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg') + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -55,7 +65,6 @@ INSTALLED_APPS = [ # Apps with multiple or complex dependencies 'emailPremium', - 'testPlugin', # Test plugin 'emailMarketing', # Depends on websiteFunctions and loginSystem 'cloudAPI', # Depends on websiteFunctions 'containerization', # Depends on websiteFunctions @@ -84,12 +93,6 @@ INSTALLED_APPS = [ # Add plugins that are installed (plugin installer handles adding/removing) # Plugins are added by plugin installer when plugins are installed -if os.path.exists('/usr/local/CyberCP/discordWebhooks/__init__.py'): - INSTALLED_APPS.append('discordWebhooks') -if os.path.exists('/usr/local/CyberCP/fail2ban/__init__.py'): - INSTALLED_APPS.append('fail2ban') -if os.path.exists('/usr/local/CyberCP/pm2Manager/__init__.py'): - INSTALLED_APPS.append('pm2Manager') MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', diff --git a/CyberCP/urls.py b/CyberCP/urls.py index 3a828e9f0..ea5ccf382 100644 --- a/CyberCP/urls.py +++ b/CyberCP/urls.py @@ -44,15 +44,14 @@ urlpatterns = [ path('filemanager/', include('filemanager.urls')), path('emailPremium/', include('emailPremium.urls')), path('manageservices/', include('manageServices.urls')), - path('plugins/testPlugin/', include('testPlugin.urls')), path('plugins/', include('pluginHolder.urls')), - path('emailMarketing/', include('emailMarketing.urls')), path('cloudAPI/', include('cloudAPI.urls')), path('docker/', include('dockerManager.urls')), path('container/', include('containerization.urls')), path('CloudLinux/', include('CLManager.urls')), path('IncrementalBackups/', include('IncBackups.urls')), path('aiscanner/', include('aiScanner.urls')), + path('emailMarketing/', include('emailMarketing.urls')), # path('Terminal/', include('WebTerminal.urls')), path('', include('loginSystem.urls')), ] diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index 4f7b020c7..f31cb4b17 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -1,8 +1,8 @@ {% load i18n %} {% get_current_language as LANGUAGE_CODE %} -{% with CP_VERSION="2.4.4.1" %} +{% with CP_VERSION="2.5.5-dev-fix" %} - + @@ -17,30 +17,39 @@ - - - - - - - + + + + - + - + @@ -1566,7 +1575,7 @@ {% block header_scripts %}{% endblock %} - + @@ -1803,9 +1812,6 @@ List Sub/Addon Domains - - Fix Subdomain Logs - {% if admin or modifyWebsite %} Modify Website @@ -2157,7 +2163,7 @@ Manage Images - + Manage Containers @@ -2287,9 +2293,6 @@ AI Scanner - - Security Management - @@ -2309,9 +2312,6 @@ SpamAssassin - - Email Marketing - MailScanner @@ -2343,11 +2343,6 @@ Manage FTP - {% if admin %} - - Bandwidth Management - - {% endif %} @@ -2452,6 +2447,12 @@ + + + {% endblock %} diff --git a/dockerManager/urls.py b/dockerManager/urls.py index 4652109c2..0292b1afd 100644 --- a/dockerManager/urls.py +++ b/dockerManager/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ re_path(r'^getTags$', views.getTags, name='getTags'), re_path(r'^runContainer', views.runContainer, name='runContainer'), re_path(r'^submitContainerCreation$', views.submitContainerCreation, name='submitContainerCreation'), + re_path(r'^containers$', views.listContainersPage, name='listContainersPage'), re_path(r'^listContainers$', views.listContainers, name='listContainers'), re_path(r'^getContainerList$', views.getContainerList, name='getContainerList'), re_path(r'^getContainerLogs$', views.getContainerLogs, name='getContainerLogs'), @@ -33,7 +34,6 @@ urlpatterns = [ re_path(r'^updateContainerPorts$', views.updateContainerPorts, name='updateContainerPorts'), re_path(r'^manageNetworks$', views.manageNetworks, name='manageNetworks'), re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'), - re_path(r'^listContainers$', views.listContainers, name='listContainers'), re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'), re_path(r'^deleteContainerKeepData$', views.deleteContainerKeepData, name='deleteContainerKeepData'), re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'), diff --git a/dockerManager/views.py b/dockerManager/views.py index 8f256f1c6..356fa353c 100644 --- a/dockerManager/views.py +++ b/dockerManager/views.py @@ -179,14 +179,40 @@ def runContainer(request): return redirect(loadLoginPage) @preDockerRun -def listContainers(request): +def listContainersPage(request): + """ + GET /docker/containers: Render HTML page only. Separate URL avoids + cache/proxy ever serving JSON (listContainers used to return JSON). + """ try: userID = request.session['userID'] cm = ContainerManager() - return cm.listContainers(request, userID) + resp = cm.listContainers(request, userID) + resp['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' + resp['Pragma'] = 'no-cache' + resp['Expires'] = '0' + return resp except KeyError: return redirect(loadLoginPage) + +@preDockerRun +def listContainers(request): + """ + GET: Redirect to /docker/containers (HTML). POST: 405. + listContainers URL historically returned JSON; caches may serve stale JSON. + Use /docker/containers for the page to avoid that. + """ + try: + request.session['userID'] # ensure logged in + except KeyError: + return redirect(loadLoginPage) + + if request.method != 'GET': + return HttpResponse('Method Not Allowed', status=405) + from django.urls import reverse + return redirect(reverse('listContainersPage')) + @preDockerRun def getContainerLogs(request): try: @@ -746,27 +772,6 @@ def getContainerEnv(request): except KeyError: return redirect(loadLoginPage) -@preDockerRun -def listContainers(request): - """ - Get list of all Docker containers - """ - try: - userID = request.session['userID'] - currentACL = ACLManager.loadedACL(userID) - - if currentACL['admin'] == 1: - pass - else: - return ACLManager.loadErrorJson() - - cm = ContainerManager() - coreResult = cm.listContainers(userID) - - return coreResult - except KeyError: - return redirect(loadLoginPage) - @preDockerRun def getDockerNetworks(request): """ diff --git a/paypalPremiumPlugin/views.py b/paypalPremiumPlugin/views.py new file mode 100644 index 000000000..c16bc0337 --- /dev/null +++ b/paypalPremiumPlugin/views.py @@ -0,0 +1,496 @@ +# -*- coding: utf-8 -*- +""" +PayPal Premium Plugin Views - Enhanced Security Version +This version uses remote server verification with multiple security layers +SECURITY: All PayPal verification happens on YOUR server, not user's server +""" + +from django.shortcuts import render, redirect +from django.http import JsonResponse +from plogical.mailUtilities import mailUtilities +from plogical.httpProc import httpProc +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging +from functools import wraps +import sys +import os +import urllib.request +import urllib.error +import json +import hashlib +import socket +import platform +import subprocess +import time +import uuid + +# Remote verification server (YOUR server, not user's server) +REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-paypal-payment' +PLUGIN_NAME = 'paypalPremiumPlugin' # PayPal Premium Plugin Example +PLUGIN_VERSION = '1.0.0' + +# PayPal configuration +PAYPAL_ME_URL = 'https://paypal.me/KimBS?locale.x=en_US&country.x=NO' +PAYPAL_PAYMENT_LINK = '' # Can be set to a PayPal Payment Link URL + +# Security configuration +CACHE_FILE = '/tmp/.paypalPremiumPlugin_license_cache' +CACHE_DURATION = 3600 # 1 hour + +# File integrity hashes (generated after plugin finalization) +# To regenerate: python3 -c "import hashlib; print(hashlib.sha256(open('views.py', 'rb').read()).hexdigest())" +PLUGIN_FILE_HASHES = { + 'views.py': '4899d70dde220b38d691a5cefdc4fd77b6d3e250ac1c7e12fa280d6f4ad31eb1', # Updated with security features + 'urls.py': '92433d401c358cd33ffd1926881920fd1867bb6d7dad1c3c2ed1e7d3b0abc2c6', +} + +def get_server_fingerprint(): + """ + Generate unique server fingerprint + Ties license to specific server hardware/configuration + """ + fingerprint_data = [] + + try: + # Server hostname + fingerprint_data.append(socket.gethostname()) + + # Primary IP + fingerprint_data.append(socket.gethostbyname(socket.gethostname())) + + # System information + fingerprint_data.append(platform.node()) + fingerprint_data.append(platform.machine()) + fingerprint_data.append(platform.processor()) + + # MAC address + fingerprint_data.append(str(uuid.getnode())) + + # Disk information (if available) + try: + result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True, timeout=2) + fingerprint_data.append(result.stdout[:100]) + except: + pass + + # Create hash + fingerprint_string = '|'.join(str(x) for x in fingerprint_data) + return hashlib.sha256(fingerprint_string.encode()).hexdigest() + except Exception as e: + # Fallback fingerprint + return hashlib.sha256(f"{socket.gethostname()}|{platform.node()}".encode()).hexdigest() + +def verify_code_integrity(): + """ + Verify plugin files haven't been tampered with + Returns: (is_valid, error_message) + """ + plugin_dir = os.path.dirname(os.path.abspath(__file__)) + + for filename, expected_hash in PLUGIN_FILE_HASHES.items(): + if not expected_hash: + continue # Skip if hash not set + + filepath = os.path.join(plugin_dir, filename) + if os.path.exists(filepath): + try: + with open(filepath, 'rb') as f: + file_content = f.read() + file_hash = hashlib.sha256(file_content).hexdigest() + + if file_hash != expected_hash: + return False, f"File {filename} has been modified (integrity check failed)" + except Exception as e: + return False, f"Error checking {filename}: {str(e)}" + + return True, None + +def get_cached_verification(): + """Get cached verification result""" + if os.path.exists(CACHE_FILE): + try: + with open(CACHE_FILE, 'r') as f: + cache_data = json.load(f) + cache_time = cache_data.get('timestamp', 0) + server_fp = cache_data.get('server_fingerprint') + + # Verify server fingerprint matches + current_fp = get_server_fingerprint() + if server_fp != current_fp: + return None # Server changed, invalidate cache + + # Check if cache is still valid + if time.time() - cache_time < CACHE_DURATION: + return cache_data.get('has_access', False) + except: + pass + return None + +def cache_verification_result(has_access, server_fp): + """Cache verification result""" + try: + with open(CACHE_FILE, 'w') as f: + json.dump({ + 'has_access': has_access, + 'server_fingerprint': server_fp, + 'timestamp': time.time() + }, f) + os.chmod(CACHE_FILE, 0o600) # Secure permissions (owner read/write only) + except Exception as e: + pass # Silently fail caching + +def cyberpanel_login_required(view_func): + """ + Custom decorator that checks for CyberPanel session userID + """ + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + try: + userID = request.session['userID'] + # User is authenticated via CyberPanel session + return view_func(request, *args, **kwargs) + except KeyError: + # Not logged in, redirect to login + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + return _wrapped_view + +def secure_verification_required(view_func): + """ + Enhanced decorator with multiple security checks + """ + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + # Check 1: Login required + try: + userID = request.session['userID'] + except KeyError: + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + + # Check 2: Code integrity + is_valid, integrity_error = verify_code_integrity() + if not is_valid: + # Log security violation + logging.writeToFile(f"SECURITY VIOLATION: {integrity_error} - User: {request.session.get('userID')}") + + # Show error (don't reveal details) + context = { + 'error': 'Plugin integrity check failed. Please reinstall the plugin.', + 'security_violation': True + } + proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin') + return proc.render() + + # Check 3: Remote verification + user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None + if not user_email: + user_email = request.session.get('email', '') or getattr(request.user, 'username', '') + + domain = request.get_host() + user_ip = request.META.get('REMOTE_ADDR', '') + + verification_result = check_remote_payment_secure( + user_email, + user_ip, + domain + ) + + if not verification_result.get('has_access', False): + # Show payment required page + context = { + 'plugin_name': 'PayPal Premium Plugin Example', + 'is_paid': True, + 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': verification_result.get('message', 'PayPal payment required'), + 'error': verification_result.get('error') + } + proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin') + return proc.render() + + # All checks passed - proceed + return view_func(request, *args, **kwargs) + + return _wrapped_view + +def remote_verification_required(view_func): + """ + Decorator that checks PayPal payment via remote server + No secrets stored in plugin - all verification happens on your server + """ + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + # First check login + try: + userID = request.session['userID'] + except KeyError: + from loginSystem.views import loadLoginPage + return redirect(loadLoginPage) + + # Get user email + user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None + if not user_email: + # Try to get from session or username + user_email = request.session.get('email', '') or getattr(request.user, 'username', '') + + # Check payment via remote server + verification_result = check_remote_payment_secure( + user_email, + request.META.get('REMOTE_ADDR', ''), + request.get_host() + ) + + if not verification_result.get('has_access', False): + # User doesn't have payment - show payment required page + context = { + 'plugin_name': 'PayPal Premium Plugin Example', + 'is_paid': True, + 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': verification_result.get('message', 'PayPal payment required'), + 'error': verification_result.get('error') + } + proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin') + return proc.render() + + # User has access - proceed with view + return view_func(request, *args, **kwargs) + + return _wrapped_view + +def check_remote_payment_secure(user_email, user_ip='', domain=''): + """ + Enhanced remote payment verification with multiple security layers + + Args: + user_email: User's email address + user_ip: User's IP address (for logging/security) + domain: Current domain (for domain binding) + + Returns: + dict: { + 'has_access': bool, + 'paypal_me_url': str, + 'paypal_payment_link': str, + 'message': str, + 'error': str or None + } + """ + # Layer 1: Code integrity check + is_valid, integrity_error = verify_code_integrity() + if not is_valid: + return { + 'has_access': False, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Plugin integrity check failed', + 'error': integrity_error, + 'security_violation': True + } + + # Layer 2: Check cache + cached_result = get_cached_verification() + if cached_result is not None: + return { + 'has_access': cached_result, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Access granted' if cached_result else 'PayPal payment required' + } + + # Layer 3: Server fingerprinting + server_fp = get_server_fingerprint() + + # Layer 4: Prepare secure request + request_data = { + 'user_email': user_email, + 'plugin_name': PLUGIN_NAME, + 'plugin_version': PLUGIN_VERSION, + 'server_fingerprint': server_fp, + 'domain': domain, + 'user_ip': user_ip, + 'timestamp': int(time.time()) + } + + try: + # Make request to remote verification server + req = urllib.request.Request( + REMOTE_VERIFICATION_URL, + data=json.dumps(request_data).encode('utf-8'), + headers={ + 'Content-Type': 'application/json', + 'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}', + 'X-Plugin-Name': PLUGIN_NAME, + 'X-Timestamp': str(request_data['timestamp']) + } + ) + + # Send request with timeout + try: + with urllib.request.urlopen(req, timeout=10) as response: + response_data = json.loads(response.read().decode('utf-8')) + + if response_data.get('success', False): + has_access = response_data.get('has_access', False) + + # Cache result + cache_verification_result(has_access, server_fp) + + return { + 'has_access': has_access, + 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': response_data.get('message', 'Access granted' if has_access else 'PayPal payment required'), + 'error': None + } + else: + return { + 'has_access': False, + 'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'message': response_data.get('message', 'PayPal payment required'), + 'error': response_data.get('error') + } + except urllib.error.HTTPError as e: + # Server returned error + error_body = e.read().decode('utf-8') if e.fp else 'Unknown error' + return { + 'has_access': False, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Unable to verify payment. Please try again later.', + 'error': f'HTTP {e.code}: {error_body}' + } + except urllib.error.URLError as e: + # Network error + return { + 'has_access': False, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Unable to connect to verification server. Please check your internet connection.', + 'error': str(e.reason) if hasattr(e, 'reason') else str(e) + } + except Exception as e: + # Other errors + return { + 'has_access': False, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Verification error occurred. Please try again later.', + 'error': str(e) + } + + except Exception as e: + logging.writeToFile(f"Error in remote payment check: {str(e)}") + return { + 'has_access': False, + 'paypal_me_url': PAYPAL_ME_URL, + 'paypal_payment_link': PAYPAL_PAYMENT_LINK, + 'message': 'Verification error occurred. Please try again later.', + 'error': str(e) + } + +def check_remote_payment(user_email, user_ip=''): + """ + Legacy function for backward compatibility + """ + return check_remote_payment_secure(user_email, user_ip, '') + +@cyberpanel_login_required +def main_view(request): + """ + Main view for PayPal premium plugin + Shows plugin information and features if paid, or payment required message if not + """ + mailUtilities.checkHome() + + # Get user email for verification + user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None + if not user_email: + user_email = request.session.get('email', '') or getattr(request.user, 'username', '') + + # Check payment status (but don't block access) + verification_result = check_remote_payment_secure( + user_email, + request.META.get('REMOTE_ADDR', ''), + request.get_host() + ) + has_access = verification_result.get('has_access', False) + + # Determine plugin status + plugin_status = 'Active' if has_access else 'Payment Required' + + context = { + 'plugin_name': 'PayPal Premium Plugin Example', + 'version': PLUGIN_VERSION, + 'status': plugin_status, + 'has_access': has_access, + 'description': 'This is an example paid plugin that requires PayPal payment.' if not has_access else 'This is an example paid plugin. You have access because payment has been verified!', + 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'features': [ + 'Premium Feature 1', + 'Premium Feature 2', + 'Premium Feature 3', + 'Advanced Configuration', + 'Priority Support' + ] if has_access else [] + } + + proc = httpProc(request, 'paypalPremiumPlugin/index.html', context, 'admin') + return proc.render() + +@cyberpanel_login_required +def settings_view(request): + """ + Settings page for PayPal premium plugin + Shows settings but disables them if user doesn't have PayPal payment + """ + mailUtilities.checkHome() + + # Get user email for verification + user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None + if not user_email: + user_email = request.session.get('email', '') or getattr(request.user, 'username', '') + + # Check payment status (but don't block access) + verification_result = check_remote_payment_secure( + user_email, + request.META.get('REMOTE_ADDR', ''), + request.get_host() + ) + has_access = verification_result.get('has_access', False) + + # Determine plugin status + plugin_status = 'Active' if has_access else 'Payment Required' + + context = { + 'plugin_name': 'PayPal Premium Plugin Example', + 'version': PLUGIN_VERSION, + 'plugin_status': plugin_status, + 'status': plugin_status, # Keep both for compatibility + 'description': 'Configure your premium plugin settings', + 'has_access': has_access, + 'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL), + 'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK), + 'verification_message': verification_result.get('message', '') + } + + proc = httpProc(request, 'paypalPremiumPlugin/settings.html', context, 'admin') + return proc.render() + +@cyberpanel_login_required +@secure_verification_required +def api_status_view(request): + """ + API endpoint for plugin status + Only accessible with PayPal payment (verified remotely with enhanced security) + """ + return JsonResponse({ + 'plugin_name': 'PayPal Premium Plugin Example', + 'version': PLUGIN_VERSION, + 'status': 'active', + 'payment': 'verified', + 'description': 'Premium plugin is active and accessible', + 'verification_method': 'remote_secure' + }) diff --git a/pluginHolder/discordWebhooks.zip b/pluginHolder/discordWebhooks.zip new file mode 100644 index 000000000..f7cc151de Binary files /dev/null and b/pluginHolder/discordWebhooks.zip differ diff --git a/pluginHolder/fail2ban.zip b/pluginHolder/fail2ban.zip new file mode 100644 index 000000000..da2df290e Binary files /dev/null and b/pluginHolder/fail2ban.zip differ diff --git a/pluginHolder/patreon_verifier.py b/pluginHolder/patreon_verifier.py new file mode 100644 index 000000000..42fd5cfed --- /dev/null +++ b/pluginHolder/patreon_verifier.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +""" +Patreon Verifier for CyberPanel Plugins +Verifies Patreon membership status for paid plugins +""" + +import urllib.request +import urllib.error +import json +import os +import sys + +# Patreon API configuration +PATREON_API_BASE = 'https://www.patreon.com/api/oauth2/v2' +PATREON_MEMBERSHIP_TIER = 'CyberPanel Paid Plugin' # The membership tier name to check + +class PatreonVerifier: + """ + Verifies Patreon membership status for CyberPanel users + """ + + def __init__(self): + """Initialize Patreon verifier""" + # Try to import from Django settings first, then fallback to environment + try: + from django.conf import settings + self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', '')) + self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', '')) + self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', '')) + self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')) + self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')) + except: + # Fallback to environment variables only + self.client_id = os.environ.get('PATREON_CLIENT_ID', '') + self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '') + self.creator_id = os.environ.get('PATREON_CREATOR_ID', '') + self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') + self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '') + + # Cache for membership checks (to avoid excessive API calls) + self.cache_file = '/home/cyberpanel/patreon_cache.json' + self.cache_duration = 300 # Cache for 5 minutes + + def get_user_patreon_token(self, user_email): + """ + Get stored Patreon access token for a user + This should be stored when user authorizes via Patreon OAuth + """ + # In a real implementation, you'd store this in a database + # For now, we'll check if there's a stored token file + token_file = f'/home/cyberpanel/patreon_tokens/{user_email}.token' + if os.path.exists(token_file): + try: + with open(token_file, 'r') as f: + token_data = json.load(f) + return token_data.get('access_token') + except: + return None + return None + + def check_membership_cached(self, user_email): + """ + Check membership with caching + """ + import time + cache_data = self._load_cache() + cache_key = f"membership_{user_email}" + + if cache_key in cache_data: + cached_result = cache_data[cache_key] + if time.time() - cached_result.get('timestamp', 0) < self.cache_duration: + return cached_result.get('has_membership', False) + + # Check membership via API + has_membership = self.check_membership(user_email) + + # Update cache + cache_data[cache_key] = { + 'has_membership': has_membership, + 'timestamp': time.time() + } + self._save_cache(cache_data) + + return has_membership + + def check_membership(self, user_email): + """ + Check if user has active Patreon membership for 'CyberPanel Paid Plugin' + + Args: + user_email: User's email address + + Returns: + bool: True if user has active membership, False otherwise + """ + access_token = self.get_user_patreon_token(user_email) + + if not access_token: + return False + + try: + # Get user's identity + user_info = self._get_user_identity(access_token) + if not user_info: + return False + + member_id = user_info.get('id') + if not member_id: + return False + + # Get user's memberships + memberships = self._get_memberships(access_token, member_id) + if not memberships: + return False + + # Check if user has the required membership tier + # First try to match by tier ID (more accurate) + for membership in memberships: + tier_id = membership.get('id', '') + tier_name = membership.get('attributes', {}).get('title', '') + + # Check by tier ID first (most accurate) + if tier_id == self.membership_tier_id: + # Check if membership is active + status = membership.get('attributes', {}).get('patron_status', '') + if status in ['active_patron', 'former_patron']: + # Check if currently entitled + entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0) + if entitled > 0: + return True + + # Fallback: Check by tier name (for compatibility) + if PATREON_MEMBERSHIP_TIER.lower() in tier_name.lower(): + # Check if membership is active + status = membership.get('attributes', {}).get('patron_status', '') + if status in ['active_patron', 'former_patron']: + # Check if currently entitled + entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0) + if entitled > 0: + return True + + return False + + except Exception as e: + import logging + logging.writeToFile(f"Error checking Patreon membership for {user_email}: {str(e)}") + return False + + def _get_user_identity(self, access_token): + """ + Get user identity from Patreon API + """ + url = f"{PATREON_API_BASE}/identity" + + try: + req = urllib.request.Request(url) + req.add_header('Authorization', f'Bearer {access_token}') + + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + return data.get('data', {}) + except Exception as e: + import logging + logging.writeToFile(f"Error getting Patreon identity: {str(e)}") + return None + + def _get_memberships(self, access_token, member_id): + """ + Get user's memberships from Patreon API + """ + url = f"{PATREON_API_BASE}/members/{member_id}?include=currently_entitled_tiers" + + try: + req = urllib.request.Request(url) + req.add_header('Authorization', f'Bearer {access_token}') + + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + + # Parse included tiers + memberships = [] + included = data.get('included', []) + + for item in included: + if item.get('type') == 'tier': + # Include tier ID in the membership data + tier_data = { + 'id': item.get('id', ''), + 'type': item.get('type', ''), + 'attributes': item.get('attributes', {}) + } + memberships.append(tier_data) + + return memberships + except Exception as e: + import logging + logging.writeToFile(f"Error getting Patreon memberships: {str(e)}") + return [] + + def _load_cache(self): + """Load cache from file""" + if os.path.exists(self.cache_file): + try: + with open(self.cache_file, 'r') as f: + return json.load(f) + except: + return {} + return {} + + def _save_cache(self, cache_data): + """Save cache to file""" + try: + os.makedirs(os.path.dirname(self.cache_file), exist_ok=True) + with open(self.cache_file, 'w') as f: + json.dump(cache_data, f) + except Exception as e: + import logging + logging.writeToFile(f"Error saving Patreon cache: {str(e)}") + + def verify_plugin_access(self, user_email, plugin_name): + """ + Verify if user can access a paid plugin + + Args: + user_email: User's email address + plugin_name: Name of the plugin to check + + Returns: + dict: { + 'has_access': bool, + 'is_paid': bool, + 'message': str + } + """ + # Check if plugin is paid (this will be checked from meta.xml) + # For now, we'll assume the plugin system will pass this info + + # Check membership + has_membership = self.check_membership_cached(user_email) + + return { + 'has_access': has_membership, + 'is_paid': True, # This will be determined by plugin metadata + 'message': 'Access granted' if has_membership else 'Patreon subscription required' + } diff --git a/pluginHolder/plugin_access.py b/pluginHolder/plugin_access.py new file mode 100644 index 000000000..f460e0768 --- /dev/null +++ b/pluginHolder/plugin_access.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +""" +Plugin Access Control +Checks if user has access to paid plugins +""" + +from .patreon_verifier import PatreonVerifier +import logging + +def check_plugin_access(request, plugin_name, plugin_meta=None): + """ + Check if user has access to a plugin + + Args: + request: Django request object + plugin_name: Name of the plugin + plugin_meta: Plugin metadata dict (optional, will be loaded if not provided) + + Returns: + dict: { + 'has_access': bool, + 'is_paid': bool, + 'message': str, + 'patreon_url': str or None + } + """ + # Default response for free plugins + default_response = { + 'has_access': True, + 'is_paid': False, + 'message': 'Access granted', + 'patreon_url': None + } + + # If plugin_meta not provided, try to load it + if plugin_meta is None: + plugin_meta = _load_plugin_meta(plugin_name) + + # Check if plugin is paid + if not plugin_meta or not plugin_meta.get('is_paid', False): + return default_response + + # Plugin is paid - check Patreon membership + if not request.user or not request.user.is_authenticated: + return { + 'has_access': False, + 'is_paid': True, + 'message': 'Please log in to access this plugin', + 'patreon_url': plugin_meta.get('patreon_url') + } + + # Get user email + user_email = getattr(request.user, 'email', None) + if not user_email: + # Try to get from username or other fields + user_email = getattr(request.user, 'username', '') + + if not user_email: + return { + 'has_access': False, + 'is_paid': True, + 'message': 'Unable to verify user identity', + 'patreon_url': plugin_meta.get('patreon_url') + } + + # Check Patreon membership + verifier = PatreonVerifier() + has_membership = verifier.check_membership_cached(user_email) + + if has_membership: + return { + 'has_access': True, + 'is_paid': True, + 'message': 'Access granted', + 'patreon_url': None + } + else: + return { + 'has_access': False, + 'is_paid': True, + 'message': f'This plugin requires a Patreon subscription to "{plugin_meta.get("patreon_tier", "CyberPanel Paid Plugin")}"', + 'patreon_url': plugin_meta.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership') + } + +def _load_plugin_meta(plugin_name): + """ + Load plugin metadata from meta.xml + + Args: + plugin_name: Name of the plugin + + Returns: + dict: Plugin metadata or None + """ + import os + from xml.etree import ElementTree + + installed_path = f'/usr/local/CyberCP/{plugin_name}/meta.xml' + source_path = f'/home/cyberpanel/plugins/{plugin_name}/meta.xml' + + meta_path = None + if os.path.exists(installed_path): + meta_path = installed_path + elif os.path.exists(source_path): + meta_path = source_path + + if not meta_path: + return None + + try: + tree = ElementTree.parse(meta_path) + root = tree.getroot() + + # Extract paid plugin information + paid_elem = root.find('paid') + patreon_tier_elem = root.find('patreon_tier') + patreon_url_elem = root.find('patreon_url') + + is_paid = False + if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true': + is_paid = True + + return { + 'is_paid': is_paid, + 'patreon_tier': patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin', + 'patreon_url': patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership' + } + except Exception as e: + logging.writeToFile(f"Error loading plugin meta for {plugin_name}: {str(e)}") + return None diff --git a/pluginHolder/templates/pluginHolder/plugin_help.html b/pluginHolder/templates/pluginHolder/plugin_help.html new file mode 100644 index 000000000..40b340c67 --- /dev/null +++ b/pluginHolder/templates/pluginHolder/plugin_help.html @@ -0,0 +1,351 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Plugin Help - " %}{{ plugin_name }} - CyberPanel{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +{% load static %} + +
+
+ +
+

+
+ +
+ {% trans "Module Help" %} +

+
{{ plugin_name }}
+ {% if plugin_description %} +

+ {{ plugin_description }} +

+ {% endif %} +
+
+ + {% trans "Version:" %} {{ plugin_version }} +
+
+ + {% trans "Author:" %} {{ plugin_author }} +
+ {% if installed %} +
+ + {% trans "Status:" %} {% trans "Installed" %} +
+ {% endif %} +
+
+ + +
+
+ {{ help_content|safe }} +
+
+ + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/pluginHolder/templates/pluginHolder/plugin_not_found.html b/pluginHolder/templates/pluginHolder/plugin_not_found.html new file mode 100644 index 000000000..9d0af1f7f --- /dev/null +++ b/pluginHolder/templates/pluginHolder/plugin_not_found.html @@ -0,0 +1,93 @@ +{% extends "baseTemplate/index.html" %} +{% load i18n %} +{% block title %}{% trans "Plugin Not Found - CyberPanel" %}{% endblock %} + +{% block header_scripts %} + +{% endblock %} + +{% block content %} +
+
+
+ +
+

{% trans "Plugin Not Found" %}

+

+ {% if plugin_name %} + {% trans "The plugin" %} "{{ plugin_name }}" {% trans "could not be found." %} + {% else %} + {% trans "The requested plugin could not be found." %} + {% endif %} + {% if error %} +
{{ error }} + {% endif %} +

+ +
+
+{% endblock %} \ No newline at end of file diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index f996b8329..089569bc6 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -182,6 +182,103 @@ color: var(--text-secondary, #64748b); } + .plugin-pricing-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: 6px; + vertical-align: middle; + } + + .plugin-pricing-badge.free { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + .plugin-pricing-badge.paid { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; + } + + .paid-badge { + display: inline-block; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 4px 10px; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-left: 8px; + vertical-align: middle; + } + + .paid-badge i { + margin-right: 4px; + } + + .subscription-warning { + background: #fff3cd; + border: 1px solid #ffc107; + border-radius: 8px; + padding: 12px; + margin-top: 10px; + font-size: 12px; + color: #856404; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + } + + .subscription-warning-content { + display: flex; + align-items: center; + flex: 1; + } + + .subscription-warning i { + margin-right: 6px; + color: #ffc107; + } + + .subscription-warning-button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: linear-gradient(135deg, #f96854 0%, #f96854 100%); + color: white; + text-decoration: none; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + transition: all 0.3s ease; + white-space: nowrap; + box-shadow: 0 2px 4px rgba(249, 104, 84, 0.3); + } + + .subscription-warning-button:hover { + background: linear-gradient(135deg, #e55a47 0%, #e55a47 100%); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(249, 104, 84, 0.4); + color: white; + text-decoration: none; + } + + .subscription-warning-button i { + margin-right: 0; + color: white; + } + .plugin-description { font-size: 13px; color: var(--text-secondary, #64748b); @@ -803,30 +900,49 @@

{% trans "Plugins" %}

+ {% if plugins %} -
- - - @@ -836,7 +952,6 @@
- {% if plugins %}
{% for plugin in plugins %} @@ -856,22 +971,35 @@ {% endif %}
-

{{ plugin.name }}

+

{{ plugin.name }}{% if plugin.is_paid|default:False|default_if_none:False %} {% endif %}

{{ plugin.type }} v{{ plugin.version }} + {% if plugin.is_paid|default:False|default_if_none:False %} + + {% else %} + {% trans "Free" %} + {% endif %}
- {% if plugin.author %} -
- - {% trans "Author:" %} {{ plugin.author }} -
- {% endif %}
{{ plugin.desc }} + {% if plugin.is_paid|default:False|default_if_none:False %} +
+
+ + {% trans "Paid Plugin:" %} {% trans "Requires Patreon subscription to" %} "{{ plugin.patreon_tier|default:'CyberPanel Paid Plugin' }}" +
+ {% if plugin.patreon_url %} + + + {% trans "Subscribe on Patreon" %} + + {% endif %} +
+ {% endif %}
@@ -943,7 +1071,6 @@ {% trans "Plugin Name" %} - {% trans "Author" %} {% trans "Version" %} {% trans "Modify Date" %} {% trans "Status" %} @@ -959,13 +1086,13 @@ {{ plugin.name }} - - - {{ plugin.author|default:"Unknown" }} - - {{ plugin.version }} + {% if plugin.is_paid|default:False|default_if_none:False %} + + {% else %} + {% trans "Free" %} + {% endif %} @@ -1043,6 +1170,26 @@

{% trans "No Plugins Installed" %}

{% trans "You haven't installed any plugins yet. Plugins extend CyberPanel's functionality with additional features." %}

+ + +
+ + + + + + {% trans "Plugin Development Guide" %} + +
{% endif %} @@ -1097,13 +1244,12 @@ + - + - - @@ -1125,6 +1271,8 @@
{% trans "Icon" %} {% trans "Plugin Name" %}{% trans "Author" %} {% trans "Version" %}{% trans "Pricing" %} {% trans "Modify Date" %}{% trans "Status" %} {% trans "Action" %}{% trans "Active" %} {% trans "Help" %} {% trans "About" %}