user level ssh terminal

This commit is contained in:
usmannasir
2025-05-18 13:43:51 +05:00
parent be923247c0
commit 9cd851c4e6
8 changed files with 773 additions and 50 deletions

153
fastapi_ssh_server.py Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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) {
}
});

View File

@@ -7,57 +7,245 @@
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<div class="container">
<div id="page-title">
<h2>{% trans "SSH Access" %}</h2>
<p>{% trans "Set up SSH access and enable/disable CageFS for " %} {{ domainName }}. {% trans " CageFS require CloudLinux OS." %}</p>
</div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<style>
body {
background: #f6f8fa;
}
.ssh-access-panel, .ssh-access-panel * {
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
}
.ssh-access-panel {
width: 100%;
max-width: 1100px;
margin: 48px auto 0 auto;
background: linear-gradient(180deg, #fff 80%, #f6f8fa 100%);
border-radius: 18px;
box-shadow: 0 4px 32px #e0e7ef;
padding: 48px 48px 32px 48px;
position: relative;
border: 1px solid #e3e8ee;
}
#page-title h2 {
font-size: 2.1em;
color: #222b45;
font-weight: 700;
margin-bottom: 8px;
}
#page-title p {
color: #7b8190;
font-size: 1.08em;
margin-bottom: 24px;
}
.open-terminal-float {
position: absolute;
right: 48px;
top: 48px;
z-index: 2;
}
.open-terminal-float .btn-success {
padding: 13px 36px;
font-size: 1.13em;
font-weight: 700;
border-radius: 8px;
box-shadow: 0 2px 8px #b2ebf2;
background: linear-gradient(90deg, #00b894 60%, #17a2b8 100%);
border: none;
transition: background 0.2s, box-shadow 0.2s;
}
.open-terminal-float .btn-success:hover {
background: linear-gradient(90deg, #17a2b8 60%, #00b894 100%);
box-shadow: 0 4px 16px #b2ebf2;
}
.panel {
background: none;
border: none;
box-shadow: none;
}
.panel-body {
padding: 0;
}
.title-hero {
font-size: 1.2em;
color: #2563eb;
font-weight: 600;
margin-bottom: 18px;
}
.form-group {
margin-bottom: 22px;
}
.form-control {
border-radius: 6px;
border: 1px solid #d1d5db;
font-size: 1.08em;
padding: 10px 12px;
}
.btn-primary, .btn-success {
min-width: 140px;
min-height: 48px;
padding: 0 32px;
border-radius: 6px;
font-weight: 600;
font-size: 1.08em;
box-shadow: 0 2px 8px #e3e8ee;
border: none;
transition: background 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1.2;
}
.btn-primary {
background: linear-gradient(90deg, #2563eb 60%, #007bff 100%);
color: #fff;
}
.btn-primary:hover {
background: linear-gradient(90deg, #007bff 60%, #2563eb 100%);
box-shadow: 0 4px 16px #e3e8ee;
}
.btn-success {
background: linear-gradient(90deg, #00b894 60%, #17a2b8 100%);
color: #fff;
}
.btn-success:hover {
background: linear-gradient(90deg, #17a2b8 60%, #00b894 100%);
box-shadow: 0 4px 16px #b2ebf2;
}
.ssh-btn-row {
display: flex;
gap: 16px;
margin-top: 12px;
align-items: center;
}
.table {
background: #f8fafc;
border-radius: 8px;
overflow: hidden;
margin-bottom: 0;
}
.table th {
background: #f1f5f9;
color: #222b45;
font-weight: 700;
border: none;
}
.table td {
background: #fff;
color: #444;
border-top: 1px solid #e3e8ee;
}
.h4.text-danger.text-bold {
color: #e74c3c !important;
font-weight: 700;
font-size: 1.2em;
cursor: pointer;
}
.alert-warning {
border-radius: 8px;
font-size: 1em;
}
.btn.btn-border.btn-alt.border-red.btn-link.font-red {
border-radius: 6px;
font-weight: 600;
font-size: 1em;
}
.btn.btn-warning {
border-radius: 6px;
font-weight: 600;
font-size: 1em;
}
.table tbody tr:nth-child(even) {
@media (max-width: 1200px) {
.ssh-access-panel {
padding: 24px 8px 16px 8px;
}
.open-terminal-float {
right: 10px;
top: 10px;
}
}
</style>
<div class="ssh-access-panel">
<div ng-controller="sshAccess" class="panel">
<div id="page-title" style="display: flex; align-items: center; justify-content: space-between;">
<div>
<h2 class="ssh-access-title" style="margin-bottom: 0;">{% trans "SSH Access" %}</h2>
<p style="margin-top: 4px;">{% trans "Set up SSH access and enable/disable CageFS for " %} {{ domainName }}. {% trans " CageFS require CloudLinux OS." %}</p>
</div>
<div style="margin-left: 24px;">
<button type="button" class="btn btn-success" ng-click="openWebTerminal()" style="min-height: 48px; padding: 0 32px; font-size: 1.13em; font-weight: 700; border-radius: 8px; box-shadow: 0 2px 8px #b2ebf2; background: linear-gradient(90deg, #00b894 60%, #17a2b8 100%); border: none; transition: background 0.2s, box-shadow 0.2s; display: flex; align-items: center; justify-content: center;">
<span class="btn-content" style="display: flex; align-items: center; gap: 8px;"><span style="font-size: 1.2em;">🖥️</span> Open Terminal</span>
</button>
<div id="web-terminal-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Web SSH Terminal</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{% if not has_addons %}
<div style="background: #fff3cd; color: #856404; border: 1px solid #ffeeba; border-radius: 8px; padding: 18px; margin-bottom: 18px; text-align: center;">
<strong>This feature requires the CyberPanel Add-ons bundle.</strong><br>
<a href="https://cyberpanel.net/cyberpanel-addons" target="_blank" style="color: #2563eb; text-decoration: underline; font-weight: 600;">Learn more & upgrade</a>
</div>
<div style="position: relative; width: 100%; height: 400px;">
<div id="xterm-container" style="width:100%;height:400px;background:#000;"></div>
<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.85); display: flex; align-items: center; justify-content: center; z-index: 10; border-radius: 8px; font-size: 1.2em; color: #888; font-weight: 600;">
Web Terminal is disabled. Please upgrade to CyberPanel Add-ons to enable this feature.
</div>
</div>
{% else %}
<div id="xterm-container" style="width:100%;height:400px;background:#000;"></div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="panel-body">
{% if is_selfsigned_ssl %}
<div class="alert alert-warning ssh-access-warning">
<strong>Warning:</strong> Your server is using a <b>self-signed SSL certificate</b> for the web terminal.<br>
For security and browser compatibility, please issue a valid hostname SSL certificate.<br>
<a href="{{ ssl_issue_link }}" target="_blank" class="btn btn-warning" style="margin-top:10px;">Issue SSL Now</a>
</div>
{% endif %}
<h3 class="title-hero">
{% trans "Set up SSH access for " %} {{ domainName }}.</span> <img ng-hide="wpInstallLoading"
src="{% static 'images/loading.gif' %}"> - <a target="_blank" href="https://go.cyberpanel.net/SFTPAccess" style="height: 23px;line-height: 21px;" class="btn btn-border btn-alt border-red btn-link font-red" title=""><span>SFTP Docs</span></a>
{% trans "Set up SSH access for " %} {{ domainName }}.
<img ng-hide="wpInstallLoading" src="{% static 'images/loading.gif' %}"> -
<a target="_blank" href="https://go.cyberpanel.net/SFTPAccess" style="height: 23px;line-height: 21px;" class="btn btn-border btn-alt border-red btn-link font-red" title=""><span>SFTP Docs</span></a>
</h3>
<div class="example-box-wrapper">
<form name="websiteCreationForm" action="/" id="createPackages"
class="form-horizontal bordered-row">
<form name="websiteCreationForm" action="/" id="createPackages" class="form-horizontal bordered-row">
<div ng-hide="installationDetailsForm" class="form-group">
<label class="col-sm-3 control-label"></label>
<label class="col-sm-4 control-label">{% trans "SSH user for " %}
<spam id="domainName">{{ domainName }}</spam>
<span id="domainName">{{ domainName }}</span>
{% trans ' is ' %} <span id="externalApp">{{ externalApp }}</span></label>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" class="form-control" ng-model="password" required>
</div>
</div>
<div ng-hide="installationDetailsForm" class="form-group">
<label class="col-sm-3 control-label"></label>
<div class="col-sm-4">
<button type="button" ng-click="setupSSHAccess()"
class="btn btn-primary btn-lg btn-block">{% trans "Save Changes" %}</button>
<button type="button" ng-click="setupSSHAccess()" class="btn btn-primary btn-lg btn-block">{% trans "Save Changes" %}</button>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<form action="/" class="form-horizontal bordered-row">
<!------ List of records --------------->
<div class="form-group">
<div class="col-sm-12">
<table class="table">
<thead>
<tr>
@@ -78,47 +266,30 @@
</table>
</div>
</div>
<!------ List of records --------------->
<div ng-hide="keyBox" class="form-group">
<div class="col-sm-12">
<textarea placeholder="Paste your public key here..." ng-model="keyData"
rows="6" class="form-control">{{ logs }}</textarea>
<textarea placeholder="Paste your public key here..." ng-model="keyData" rows="6" class="form-control">{{ logs }}</textarea>
</div>
</div>
<div ng-hide="showKeyBox" class="form-group">
<label class="col-sm-3 control-label"></label>
<div class="col-sm-4">
<button type="button" ng-click="addKey()"
class="btn btn-primary btn-lg">{% trans "Add Key" %}</button>
<div class="col-sm-4 ssh-btn-row">
<button type="button" ng-click="addKey()" class="btn btn-primary">{% trans "Add Key" %}</button>
</div>
</div>
<div ng-hide="saveKeyBtn" class="form-group">
<label class="col-sm-3 control-label"></label>
<div class="col-sm-4">
<button type="button" ng-click="saveKey()"
class="btn btn-primary btn-lg">{% trans "Save" %}</button>
<div class="col-sm-4 ssh-btn-row">
<button type="button" ng-click="saveKey()" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</div>
</form>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -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('<domain>/<childDomain>', views.launchChild, name='launchChild'),
path('<domain>', views.domain, name='domain'),
path('get_website_resources/', views.get_website_resources, name='get_website_resources'),
]

View File

@@ -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']
@@ -1923,3 +1977,53 @@ 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)})
@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)})

View File

@@ -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):
@@ -7259,3 +7357,5 @@ StrictHostKeyChecking no
data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)