mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-11-07 13:56:01 +01:00
user level ssh terminal
This commit is contained in:
153
fastapi_ssh_server.py
Normal file
153
fastapi_ssh_server.py
Normal 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()
|
||||
14
fastapi_ssh_server.service
Normal file
14
fastapi_ssh_server.service
Normal 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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -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">×</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 %}
|
||||
@@ -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'),
|
||||
|
||||
|
||||
]
|
||||
|
||||
@@ -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)})
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user