From 9cd851c4e6b7b752b2905de83ebaa93471ea5133 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sun, 18 May 2025 13:43:51 +0500 Subject: [PATCH] user level ssh terminal --- fastapi_ssh_server.py | 153 ++++++++++ fastapi_ssh_server.service | 14 + requirments-old.txt | 6 + .../websiteFunctions/websiteFunctions.js | 174 +++++++++++- .../templates/websiteFunctions/sshAccess.html | 261 +++++++++++++++--- websiteFunctions/urls.py | 3 + websiteFunctions/views.py | 108 +++++++- websiteFunctions/website.py | 104 ++++++- 8 files changed, 773 insertions(+), 50 deletions(-) create mode 100644 fastapi_ssh_server.py create mode 100644 fastapi_ssh_server.service diff --git a/fastapi_ssh_server.py b/fastapi_ssh_server.py new file mode 100644 index 000000000..d5d5a1f53 --- /dev/null +++ b/fastapi_ssh_server.py @@ -0,0 +1,153 @@ +import asyncio +import asyncssh +import tempfile +import os +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query +from fastapi.middleware.cors import CORSMiddleware +import paramiko # For key generation and manipulation +import io +import pwd +from jose import jwt, JWTError +import logging + +app = FastAPI() +JWT_SECRET = "YOUR_SECRET_KEY" +JWT_ALGORITHM = "HS256" + +# Allow CORS for local dev/testing +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +SSH_USER = "your_website_user" # Replace with a real user for testing +AUTHORIZED_KEYS_PATH = f"/home/{SSH_USER}/.ssh/authorized_keys" + +# Helper to generate a keypair +def generate_ssh_keypair(): + key = paramiko.RSAKey.generate(2048) + private_io = io.StringIO() + key.write_private_key(private_io) + private_key = private_io.getvalue() + public_key = f"{key.get_name()} {key.get_base64()}" + return private_key, public_key + +# Add public key to authorized_keys with a unique comment +def add_key_to_authorized_keys(public_key, comment): + entry = f'from="127.0.0.1" {public_key} {comment}\n' + with open(AUTHORIZED_KEYS_PATH, "a") as f: + f.write(entry) + +# Remove public key from authorized_keys by comment +def remove_key_from_authorized_keys(comment): + with open(AUTHORIZED_KEYS_PATH, "r") as f: + lines = f.readlines() + with open(AUTHORIZED_KEYS_PATH, "w") as f: + for line in lines: + if comment not in line: + f.write(line) + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), ssh_user: str = Query(None)): + # Re-enable JWT validation + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + user = payload.get("ssh_user") + if not user: + await websocket.close() + return + except JWTError: + await websocket.close() + return + home_dir = pwd.getpwnam(user).pw_dir + ssh_dir = os.path.join(home_dir, ".ssh") + authorized_keys_path = os.path.join(ssh_dir, "authorized_keys") + + os.makedirs(ssh_dir, exist_ok=True) + if not os.path.exists(authorized_keys_path): + with open(authorized_keys_path, "w"): pass + os.chown(ssh_dir, pwd.getpwnam(user).pw_uid, pwd.getpwnam(user).pw_gid) + os.chmod(ssh_dir, 0o700) + os.chown(authorized_keys_path, pwd.getpwnam(user).pw_uid, pwd.getpwnam(user).pw_gid) + os.chmod(authorized_keys_path, 0o600) + + private_key, public_key = generate_ssh_keypair() + comment = f"webterm-{os.urandom(8).hex()}" + entry = f'from="127.0.0.1" {public_key} {comment}\n' + with open(authorized_keys_path, "a") as f: + f.write(entry) + + with tempfile.NamedTemporaryFile(delete=False) as keyfile: + keyfile.write(private_key.encode()) + keyfile_path = keyfile.name + + await websocket.accept() + conn = None + process = None + try: + conn = await asyncssh.connect( + "localhost", + username=user, + client_keys=[keyfile_path], + known_hosts=None + ) + process = await conn.create_process(term_type="xterm") + + async def ws_to_ssh(): + try: + while True: + data = await websocket.receive_bytes() + # Decode bytes to str before writing to SSH stdin + process.stdin.write(data.decode('utf-8', errors='replace')) + except WebSocketDisconnect: + process.stdin.close() + + async def ssh_to_ws(): + try: + while not process.stdout.at_eof(): + data = await process.stdout.read(1024) + if data: + # Defensive type check and logging + logging.debug(f"[ssh_to_ws] Sending to WS: type={type(data)}, sample={data[:40] if isinstance(data, bytes) else data}") + if isinstance(data, bytes): + await websocket.send_bytes(data) + elif isinstance(data, str): + await websocket.send_text(data) + else: + await websocket.send_text(str(data)) + except Exception as ex: + logging.exception(f"[ssh_to_ws] Exception: {ex}") + pass + + await asyncio.gather(ws_to_ssh(), ssh_to_ws()) + except Exception as e: + try: + # Always send error as text (string) + msg = f"Connection error: {e}" + logging.exception(f"[websocket_endpoint] Exception: {e}") + if isinstance(msg, bytes): + msg = msg.decode('utf-8', errors='replace') + await websocket.send_text(str(msg)) + except Exception as ex: + logging.exception(f"[websocket_endpoint] Error sending error message: {ex}") + pass + try: + await websocket.close() + except Exception: + pass + finally: + # Remove key from authorized_keys and delete temp private key + with open(authorized_keys_path, "r") as f: + lines = f.readlines() + with open(authorized_keys_path, "w") as f: + for line in lines: + if comment not in line: + f.write(line) + os.remove(keyfile_path) + if process: + process.close() + if conn: + conn.close() diff --git a/fastapi_ssh_server.service b/fastapi_ssh_server.service new file mode 100644 index 000000000..916b777ca --- /dev/null +++ b/fastapi_ssh_server.service @@ -0,0 +1,14 @@ +[Unit] +Description=FastAPI SSH Web Terminal Server +After=network.target + +[Service] +Type=simple +WorkingDirectory=/usr/local/CyberCP +ExecStart=/usr/local/CyberCP/bin/python3 -m uvicorn fastapi_ssh_server:app --host 0.0.0.0 --port 8888 --ssl-keyfile=/usr/local/lscp/conf/key.pem --ssl-certfile=/usr/local/lscp/conf/cert.pem +Restart=on-failure +User=root +Group=root + +[Install] +WantedBy=multi-user.target diff --git a/requirments-old.txt b/requirments-old.txt index 427741460..d4860e191 100644 --- a/requirments-old.txt +++ b/requirments-old.txt @@ -30,3 +30,9 @@ tldextract==3.0.2 tornado==6.1 validators==0.18.1 websocket-client==0.57.0 + +fastapi +uvicorn +asyncssh +python-jose +websockets \ No newline at end of file diff --git a/websiteFunctions/static/websiteFunctions/websiteFunctions.js b/websiteFunctions/static/websiteFunctions/websiteFunctions.js index ecf1752a9..807ee0cd1 100755 --- a/websiteFunctions/static/websiteFunctions/websiteFunctions.js +++ b/websiteFunctions/static/websiteFunctions/websiteFunctions.js @@ -10093,6 +10093,100 @@ function website_child_domain_checkbox_function() { app.controller('websitePages', function ($scope, $http, $timeout, $window) { + $scope.openWebTerminal = function() { + console.log('[DEBUG] openWebTerminal called'); + $('#web-terminal-modal').modal('show'); + console.log('[DEBUG] Modal should now be visible'); + + if ($scope.term) { + console.log('[DEBUG] Disposing previous terminal instance'); + $scope.term.dispose(); + } + var term = new Terminal({ + cursorBlink: true, + fontFamily: 'monospace', + fontSize: 14, + theme: { background: '#000' } + }); + $scope.term = term; + term.open(document.getElementById('xterm-container')); + term.focus(); + console.log('[DEBUG] Terminal initialized and opened'); + + // Fetch JWT from backend with CSRF token + var domain = $("#domainNamePage").text(); + var csrftoken = getCookie('csrftoken'); + console.log('[DEBUG] Fetching JWT for domain:', domain); + $http.post('/websites/getTerminalJWT', { domain: domain }, { + headers: { 'X-CSRFToken': csrftoken } + }) + .then(function(response) { + console.log('[DEBUG] JWT fetch response:', response); + if (response.data.status === 1 && response.data.token) { + var token = response.data.token; + var ssh_user = response.data.ssh_user; + var wsProto = location.protocol === 'https:' ? 'wss' : 'ws'; + var wsUrl = wsProto + '://' + window.location.hostname + ':8888/ws?token=' + encodeURIComponent(token) + '&ssh_user=' + encodeURIComponent(ssh_user); + console.log('[DEBUG] Connecting to WebSocket:', wsUrl); + var socket = new WebSocket(wsUrl); + socket.binaryType = 'arraybuffer'; + $scope.terminalSocket = socket; + + socket.onopen = function() { + console.log('[DEBUG] WebSocket connection opened'); + term.write('\x1b[32mConnected.\x1b[0m\r\n'); + }; + socket.onclose = function(event) { + console.log('[DEBUG] WebSocket connection closed', event); + term.write('\r\n\x1b[31mConnection closed.\x1b[0m\r\n'); + // Optionally, log modal state + console.log('[DEBUG] Modal state on close:', $('#web-terminal-modal').is(':visible')); + }; + socket.onerror = function(e) { + console.log('[DEBUG] WebSocket error', e); + term.write('\r\n\x1b[31mWebSocket error.\x1b[0m\r\n'); + }; + socket.onmessage = function(event) { + if (event.data instanceof ArrayBuffer) { + var text = new Uint8Array(event.data); + term.write(new TextDecoder().decode(text)); + } else if (typeof event.data === 'string') { + term.write(event.data); + } + }; + term.onData(function(data) { + if (socket.readyState === WebSocket.OPEN) { + var encoder = new TextEncoder(); + socket.send(encoder.encode(data)); + } + }); + term.onResize(function(size) { + if (socket.readyState === WebSocket.OPEN) { + var msg = JSON.stringify({resize: {cols: size.cols, rows: size.rows}}); + socket.send(msg); + } + }); + $('#web-terminal-modal').on('hidden.bs.modal', function() { + console.log('[DEBUG] Modal hidden event triggered'); + if ($scope.term) { + $scope.term.dispose(); + $scope.term = null; + } + if ($scope.terminalSocket) { + $scope.terminalSocket.close(); + $scope.terminalSocket = null; + } + }); + } else { + console.log('[DEBUG] Failed to get terminal token', response); + term.write('\x1b[31mFailed to get terminal token.\x1b[0m\r\n'); + } + }, function(error) { + console.log('[DEBUG] Failed to contact backend', error); + term.write('\x1b[31mFailed to contact backend.\x1b[0m\r\n'); + }); + }; + $scope.logFileLoading = true; $scope.logsFeteched = true; $scope.couldNotFetchLogs = true; @@ -14666,6 +14760,85 @@ app.controller('installMauticCTRL', function ($scope, $http, $timeout) { app.controller('sshAccess', function ($scope, $http, $timeout) { + $scope.openWebTerminal = function() { + $('#web-terminal-modal').modal('show'); + + if ($scope.term) { + $scope.term.dispose(); + } + var term = new Terminal({ + cursorBlink: true, + fontFamily: 'monospace', + fontSize: 14, + theme: { background: '#000' } + }); + $scope.term = term; + term.open(document.getElementById('xterm-container')); + term.focus(); + + // Fetch JWT from backend with CSRF token + var domain = $("#domainName").text(); + var csrftoken = getCookie('csrftoken'); + $http.post('/websites/getTerminalJWT', { domain: domain }, { + headers: { 'X-CSRFToken': csrftoken } + }) + .then(function(response) { + if (response.data.status === 1 && response.data.token) { + var token = response.data.token; + var ssh_user = $("#externalApp").text(); + var wsProto = location.protocol === 'https:' ? 'wss' : 'ws'; + var wsUrl = wsProto + '://' + window.location.hostname + ':8888/ws?token=' + encodeURIComponent(token) + '&ssh_user=' + encodeURIComponent(ssh_user); + var socket = new WebSocket(wsUrl); + socket.binaryType = 'arraybuffer'; + $scope.terminalSocket = socket; + + socket.onopen = function() { + term.write('\x1b[32mConnected.\x1b[0m\r\n'); + }; + socket.onclose = function() { + term.write('\r\n\x1b[31mConnection closed.\x1b[0m\r\n'); + }; + socket.onerror = function(e) { + term.write('\r\n\x1b[31mWebSocket error.\x1b[0m\r\n'); + }; + socket.onmessage = function(event) { + if (event.data instanceof ArrayBuffer) { + var text = new Uint8Array(event.data); + term.write(new TextDecoder().decode(text)); + } else if (typeof event.data === 'string') { + term.write(event.data); + } + }; + term.onData(function(data) { + if (socket.readyState === WebSocket.OPEN) { + var encoder = new TextEncoder(); + socket.send(encoder.encode(data)); + } + }); + term.onResize(function(size) { + if (socket.readyState === WebSocket.OPEN) { + var msg = JSON.stringify({resize: {cols: size.cols, rows: size.rows}}); + socket.send(msg); + } + }); + $('#web-terminal-modal').on('hidden.bs.modal', function() { + if ($scope.term) { + $scope.term.dispose(); + $scope.term = null; + } + if ($scope.terminalSocket) { + $scope.terminalSocket.close(); + $scope.terminalSocket = null; + } + }); + } else { + term.write('\x1b[31mFailed to get terminal token.\x1b[0m\r\n'); + } + }, function() { + term.write('\x1b[31mFailed to contact backend.\x1b[0m\r\n'); + }); + }; + $scope.wpInstallLoading = true; $scope.setupSSHAccess = function () { @@ -17837,4 +18010,3 @@ app.controller('launchChild', function ($scope, $http) { } }); - diff --git a/websiteFunctions/templates/websiteFunctions/sshAccess.html b/websiteFunctions/templates/websiteFunctions/sshAccess.html index 75305fea1..2b989e51f 100755 --- a/websiteFunctions/templates/websiteFunctions/sshAccess.html +++ b/websiteFunctions/templates/websiteFunctions/sshAccess.html @@ -7,57 +7,245 @@ {% get_current_language as LANGUAGE_CODE %} -
-
-

{% trans "SSH Access" %}

-

{% trans "Set up SSH access and enable/disable CageFS for " %} {{ domainName }}. {% trans " CageFS require CloudLinux OS." %}

-
+ + + +
+
+
+

{% trans "SSH Access" %}

+

{% trans "Set up SSH access and enable/disable CageFS for " %} {{ domainName }}. {% trans " CageFS require CloudLinux OS." %}

+
+
+ + +
+
+ {% if is_selfsigned_ssl %} +
+ Warning: Your server is using a self-signed SSL certificate for the web terminal.
+ For security and browser compatibility, please issue a valid hostname SSL certificate.
+ Issue SSL Now +
+ {% endif %}

- {% trans "Set up SSH access for " %} {{ domainName }}. - SFTP Docs + {% trans "Set up SSH access for " %} {{ domainName }}. + - + SFTP Docs

- - -
- +
-
-
- +
-
- - -
-
- @@ -78,47 +266,30 @@
- - - -
- +
- -
-
- - +
+
-
-
- +
+
-
- -
- -
- {% endblock %} \ No newline at end of file diff --git a/websiteFunctions/urls.py b/websiteFunctions/urls.py index 9b68b8712..bbf91657d 100755 --- a/websiteFunctions/urls.py +++ b/websiteFunctions/urls.py @@ -196,10 +196,13 @@ urlpatterns = [ path('statusFunc', views.statusFunc, name='statusFunc'), path('tuneSettings', views.tuneSettings, name='tuneSettings'), path('saveApacheConfigsToFile', views.saveApacheConfigsToFile, name='saveApacheConfigsToFile'), + path('getTerminalJWT', views.get_terminal_jwt, name='get_terminal_jwt'), # Catch all for domains path('/', views.launchChild, name='launchChild'), path('', views.domain, name='domain'), path('get_website_resources/', views.get_website_resources, name='get_website_resources'), + + ] diff --git a/websiteFunctions/views.py b/websiteFunctions/views.py index a1cad4436..f586e5ec6 100755 --- a/websiteFunctions/views.py +++ b/websiteFunctions/views.py @@ -19,6 +19,10 @@ from .dockerviews import startContainer as docker_startContainer from .dockerviews import stopContainer as docker_stopContainer from .dockerviews import restartContainer as docker_restartContainer from .resource_monitoring import get_website_resource_usage +import jwt +from datetime import datetime, timedelta +import OpenSSL +from plogical.processUtilities import ProcessUtilities def loadWebsitesHome(request): val = request.session['userID'] @@ -1454,13 +1458,63 @@ def prestaShopInstall(request): def sshAccess(request, domain): try: + # from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter + # # Ensure FastAPI SSH server systemd service file is in place + # try: + # service_path = '/etc/systemd/system/fastapi_ssh_server.service' + # local_service_path = 'fastapi_ssh_server.service' + # check_service = ProcessUtilities.outputExecutioner(f'test -f {service_path} && echo exists || echo missing') + # if 'missing' in check_service: + # ProcessUtilities.outputExecutioner(f'cp /usr/local/CyberCP/fastapi_ssh_server.service {service_path}') + # ProcessUtilities.outputExecutioner('systemctl daemon-reload') + # except Exception as e: + # CyberCPLogFileWriter.writeLog(f"Failed to copy or reload fastapi_ssh_server.service: {e}") + + # # Ensure FastAPI SSH server is running using ProcessUtilities + # try: + # ProcessUtilities.outputExecutioner('systemctl is-active --quiet fastapi_ssh_server') + # ProcessUtilities.outputExecutioner('systemctl enable --now fastapi_ssh_server') + # ProcessUtilities.outputExecutioner('systemctl start fastapi_ssh_server') + # except Exception as e: + # CyberCPLogFileWriter.writeLog(f"Failed to ensure fastapi_ssh_server is running: {e}") + + # # Add-on check logic + # url = "https://platform.cyberpersons.com/CyberpanelAdOns/Adonpermission" + # data = { + # "name": "all", + # "IP": ACLManager.GetServerIP() + # } + # import requests + # import json + # try: + # response = requests.post(url, data=json.dumps(data)) + # Status = response.json().get('status', 0) + # except Exception: + # Status = 0 + # has_addons = (Status == 1) or ProcessUtilities.decideServer() == ProcessUtilities.ent + + # from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter + + # CyberCPLogFileWriter.writeToFile(f"has_addons: {has_addons}") + + # userID = request.session['userID'] + # wm = WebsiteManager(domain) + # # SSL check + # cert_path = '/usr/local/lscp/conf/cert.pem' + # is_selfsigned = False + # ssl_issue_link = '/manageSSL/sslForHostName' + # try: + # cert_content = ProcessUtilities.outputExecutioner(f'cat {cert_path}') + # cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_content) + # is_selfsigned = cert.get_issuer().der() == cert.get_subject().der() + # except Exception: + # is_selfsigned = True # If cert missing or unreadable, treat as self-signed userID = request.session['userID'] wm = WebsiteManager(domain) return wm.sshAccess(request, userID) except KeyError: return redirect(loadLoginPage) - def saveSSHAccessChanges(request): try: userID = request.session['userID'] @@ -1922,4 +1976,54 @@ def get_website_resources(request): except BaseException as msg: logging.CyberCPLogFileWriter.writeToFile(f'Error in get_website_resources: {str(msg)}') - return JsonResponse({'status': 0, 'error_message': str(msg)}) \ No newline at end of file + return JsonResponse({'status': 0, 'error_message': str(msg)}) + +@csrf_exempt +def get_terminal_jwt(request): + import logging + logger = logging.getLogger("cyberpanel.ssh.jwt") + try: + logger.error("get_terminal_jwt called") + logger.error(f"Request body: {request.body}") + data = json.loads(request.body) + domain = data.get('domain') + logger.error(f"Domain: {domain}") + if not domain: + logger.error("No domain provided") + return JsonResponse({'status': 0, 'error_message': 'Domain required'}) + user_id = request.session.get('userID') + logger.error(f"User ID from session: {user_id}") + if not user_id: + logger.error("User not authenticated") + return JsonResponse({'status': 0, 'error_message': 'Not authenticated'}) + from websiteFunctions.models import Websites + from plogical.acl import ACLManager + from loginSystem.models import Administrator + admin = Administrator.objects.get(pk=user_id) + currentACL = ACLManager.loadedACL(user_id) + if ACLManager.checkOwnership(domain, admin, currentACL) != 1: + logger.error("User not authorized for domain") + return JsonResponse({'status': 0, 'error_message': 'Not authorized'}) + try: + website = Websites.objects.get(domain=domain) + except Websites.DoesNotExist: + logger.error("Website not found") + return JsonResponse({'status': 0, 'error_message': 'Website not found'}) + ssh_user = website.externalApp + logger.error(f"SSH user: {ssh_user}") + if not ssh_user: + logger.error("SSH user is empty or not set for this website.") + return JsonResponse({'status': 0, 'error_message': 'SSH user not configured for this website.'}) + from datetime import datetime, timedelta + import jwt as pyjwt + payload = { + 'user_id': user_id, + 'ssh_user': ssh_user, + 'exp': datetime.utcnow() + timedelta(minutes=10) + } + token = pyjwt.encode(payload, 'YOUR_SECRET_KEY', algorithm='HS256') + logger.error(f"JWT generated: {token}") + return JsonResponse({'status': 1, 'token': token, 'ssh_user': ssh_user}) + except Exception as e: + logger.error(f"Exception in get_terminal_jwt: {str(e)}") + return JsonResponse({'status': 0, 'error_message': str(e)}) \ No newline at end of file diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index 89605423b..477b0cde3 100755 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -3072,6 +3072,45 @@ Require valid-user else: Data['ftp'] = 0 + # Add-on check logic (copied from sshAccess) + url = "https://platform.cyberpersons.com/CyberpanelAdOns/Adonpermission" + addon_data = { + "name": "all", + "IP": ACLManager.GetServerIP() + } + import requests + import json + try: + response = requests.post(url, data=json.dumps(addon_data)) + Status = response.json().get('status', 0) + except Exception: + Status = 0 + Data['has_addons'] = bool((Status == 1) or ProcessUtilities.decideServer() == ProcessUtilities.ent) + + # SSL check (self-signed logic) + cert_path = '/etc/letsencrypt/live/%s/fullchain.pem' % (self.domain) + is_selfsigned = False + ssl_issue_link = '/manageSSL/sslForHostName' + try: + import OpenSSL + with open(cert_path, 'r') as f: + pem_data = f.read() + cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem_data) + # Only check the first cert in the PEM + issuer_org = None + for k, v in cert.get_issuer().get_components(): + if k.decode() == 'O': + issuer_org = v.decode() + break + if issuer_org == 'Denial': + is_selfsigned = True + else: + is_selfsigned = False + except Exception: + is_selfsigned = True # If cert missing or unreadable, treat as self-signed + Data['is_selfsigned_ssl'] = bool(is_selfsigned) + Data['ssl_issue_link'] = ssl_issue_link + proc = httpProc(request, 'websiteFunctions/website.html', Data) return proc.render() else: @@ -4942,8 +4981,67 @@ StrictHostKeyChecking no website = Websites.objects.get(domain=self.domain) externalApp = website.externalApp + ##### + + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter + # Ensure FastAPI SSH server systemd service file is in place + try: + service_path = '/etc/systemd/system/fastapi_ssh_server.service' + local_service_path = 'fastapi_ssh_server.service' + check_service = ProcessUtilities.outputExecutioner(f'test -f {service_path} && echo exists || echo missing') + if 'missing' in check_service: + ProcessUtilities.outputExecutioner(f'cp /usr/local/CyberCP/fastapi_ssh_server.service {service_path}') + ProcessUtilities.outputExecutioner('systemctl daemon-reload') + except Exception as e: + CyberCPLogFileWriter.writeLog(f"Failed to copy or reload fastapi_ssh_server.service: {e}") + + # Ensure FastAPI SSH server is running using ProcessUtilities + try: + ProcessUtilities.outputExecutioner('systemctl is-active --quiet fastapi_ssh_server') + ProcessUtilities.outputExecutioner('systemctl enable --now fastapi_ssh_server') + ProcessUtilities.outputExecutioner('systemctl start fastapi_ssh_server') + except Exception as e: + CyberCPLogFileWriter.writeLog(f"Failed to ensure fastapi_ssh_server is running: {e}") + + # Add-on check logic + url = "https://platform.cyberpersons.com/CyberpanelAdOns/Adonpermission" + data = { + "name": "all", + "IP": ACLManager.GetServerIP() + } + import requests + import json + try: + response = requests.post(url, data=json.dumps(data)) + Status = response.json().get('status', 0) + except Exception: + Status = 0 + has_addons = (Status == 1) or ProcessUtilities.decideServer() == ProcessUtilities.ent + + from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter + + #CyberCPLogFileWriter.writeToFile(f"has_addons: {has_addons}") + + # SSL check + cert_path = '/usr/local/lscp/conf/cert.pem' + is_selfsigned = False + ssl_issue_link = '/manageSSL/sslForHostName' + try: + import OpenSSL + cert_content = ProcessUtilities.outputExecutioner(f'cat {cert_path}') + cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_content) + ssl_provider = cert.get_issuer().get_components()[1][1].decode('utf-8') + CyberCPLogFileWriter.writeToFile(f"ssl_provider: {ssl_provider}") + if ssl_provider == 'Denial': + is_selfsigned = True + else: + is_selfsigned = False + except Exception as e: + is_selfsigned = True # If cert missing or unreadable, treat as self-signed + CyberCPLogFileWriter.writeToFile(f"is_selfsigned: {is_selfsigned}. Error: {str(e)}") + proc = httpProc(request, 'websiteFunctions/sshAccess.html', - {'domainName': self.domain, 'externalApp': externalApp}) + {'domainName': self.domain, 'externalApp': externalApp, 'has_addons': has_addons, 'is_selfsigned_ssl': is_selfsigned, 'ssl_issue_link': ssl_issue_link}) return proc.render() def saveSSHAccessChanges(self, userID=None, data=None): @@ -7258,4 +7356,6 @@ StrictHostKeyChecking no except BaseException as msg: data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)} json_data = json.dumps(data_ret) - return HttpResponse(json_data) \ No newline at end of file + return HttpResponse(json_data) + +