mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-11-07 22:06:05 +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
|
tornado==6.1
|
||||||
validators==0.18.1
|
validators==0.18.1
|
||||||
websocket-client==0.57.0
|
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) {
|
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.logFileLoading = true;
|
||||||
$scope.logsFeteched = true;
|
$scope.logsFeteched = true;
|
||||||
$scope.couldNotFetchLogs = true;
|
$scope.couldNotFetchLogs = true;
|
||||||
@@ -14666,6 +14760,85 @@ app.controller('installMauticCTRL', function ($scope, $http, $timeout) {
|
|||||||
|
|
||||||
app.controller('sshAccess', 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.wpInstallLoading = true;
|
||||||
|
|
||||||
$scope.setupSSHAccess = function () {
|
$scope.setupSSHAccess = function () {
|
||||||
@@ -17837,4 +18010,3 @@ app.controller('launchChild', function ($scope, $http) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,57 +7,245 @@
|
|||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
<!-- Current language: {{ LANGUAGE_CODE }} -->
|
||||||
|
|
||||||
<div class="container">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
|
||||||
<div id="page-title">
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||||
<h2>{% trans "SSH Access" %}</h2>
|
|
||||||
<p>{% trans "Set up SSH access and enable/disable CageFS for " %} {{ domainName }}. {% trans " CageFS require CloudLinux OS." %}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<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 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">
|
<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">
|
<h3 class="title-hero">
|
||||||
{% trans "Set up SSH access for " %} {{ domainName }}.</span> <img ng-hide="wpInstallLoading"
|
{% trans "Set up SSH access for " %} {{ domainName }}.
|
||||||
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>
|
<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>
|
</h3>
|
||||||
<div class="example-box-wrapper">
|
<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">
|
<div ng-hide="installationDetailsForm" class="form-group">
|
||||||
<label class="col-sm-3 control-label"></label>
|
<label class="col-sm-3 control-label"></label>
|
||||||
<label class="col-sm-4 control-label">{% trans "SSH user for " %}
|
<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>
|
{% trans ' is ' %} <span id="externalApp">{{ externalApp }}</span></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-3 control-label">{% trans "Password" %}</label>
|
<label class="col-sm-3 control-label">{% trans "Password" %}</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<input type="password" class="form-control" ng-model="password" required>
|
<input type="password" class="form-control" ng-model="password" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-hide="installationDetailsForm" class="form-group">
|
<div ng-hide="installationDetailsForm" class="form-group">
|
||||||
<label class="col-sm-3 control-label"></label>
|
<label class="col-sm-3 control-label"></label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<button type="button" ng-click="setupSSHAccess()"
|
<button type="button" ng-click="setupSSHAccess()" class="btn btn-primary btn-lg btn-block">{% trans "Save Changes" %}</button>
|
||||||
class="btn btn-primary btn-lg btn-block">{% trans "Save Changes" %}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<form action="/" class="form-horizontal bordered-row">
|
<form action="/" class="form-horizontal bordered-row">
|
||||||
|
|
||||||
<!------ List of records --------------->
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -78,47 +266,30 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!------ List of records --------------->
|
|
||||||
|
|
||||||
<div ng-hide="keyBox" class="form-group">
|
<div ng-hide="keyBox" class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<textarea placeholder="Paste your public key here..." ng-model="keyData"
|
<textarea placeholder="Paste your public key here..." ng-model="keyData" rows="6" class="form-control">{{ logs }}</textarea>
|
||||||
rows="6" class="form-control">{{ logs }}</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div ng-hide="showKeyBox" class="form-group">
|
<div ng-hide="showKeyBox" class="form-group">
|
||||||
<label class="col-sm-3 control-label"></label>
|
<label class="col-sm-3 control-label"></label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4 ssh-btn-row">
|
||||||
<button type="button" ng-click="addKey()"
|
<button type="button" ng-click="addKey()" class="btn btn-primary">{% trans "Add Key" %}</button>
|
||||||
class="btn btn-primary btn-lg">{% trans "Add Key" %}</button>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-hide="saveKeyBtn" class="form-group">
|
<div ng-hide="saveKeyBtn" class="form-group">
|
||||||
<label class="col-sm-3 control-label"></label>
|
<label class="col-sm-3 control-label"></label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4 ssh-btn-row">
|
||||||
<button type="button" ng-click="saveKey()"
|
<button type="button" ng-click="saveKey()" class="btn btn-primary">{% trans "Save" %}</button>
|
||||||
class="btn btn-primary btn-lg">{% trans "Save" %}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -196,10 +196,13 @@ urlpatterns = [
|
|||||||
path('statusFunc', views.statusFunc, name='statusFunc'),
|
path('statusFunc', views.statusFunc, name='statusFunc'),
|
||||||
path('tuneSettings', views.tuneSettings, name='tuneSettings'),
|
path('tuneSettings', views.tuneSettings, name='tuneSettings'),
|
||||||
path('saveApacheConfigsToFile', views.saveApacheConfigsToFile, name='saveApacheConfigsToFile'),
|
path('saveApacheConfigsToFile', views.saveApacheConfigsToFile, name='saveApacheConfigsToFile'),
|
||||||
|
path('getTerminalJWT', views.get_terminal_jwt, name='get_terminal_jwt'),
|
||||||
|
|
||||||
# Catch all for domains
|
# Catch all for domains
|
||||||
path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
|
path('<domain>/<childDomain>', views.launchChild, name='launchChild'),
|
||||||
path('<domain>', views.domain, name='domain'),
|
path('<domain>', views.domain, name='domain'),
|
||||||
|
|
||||||
path('get_website_resources/', views.get_website_resources, name='get_website_resources'),
|
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 stopContainer as docker_stopContainer
|
||||||
from .dockerviews import restartContainer as docker_restartContainer
|
from .dockerviews import restartContainer as docker_restartContainer
|
||||||
from .resource_monitoring import get_website_resource_usage
|
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):
|
def loadWebsitesHome(request):
|
||||||
val = request.session['userID']
|
val = request.session['userID']
|
||||||
@@ -1454,13 +1458,63 @@ def prestaShopInstall(request):
|
|||||||
|
|
||||||
def sshAccess(request, domain):
|
def sshAccess(request, domain):
|
||||||
try:
|
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']
|
userID = request.session['userID']
|
||||||
wm = WebsiteManager(domain)
|
wm = WebsiteManager(domain)
|
||||||
return wm.sshAccess(request, userID)
|
return wm.sshAccess(request, userID)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return redirect(loadLoginPage)
|
return redirect(loadLoginPage)
|
||||||
|
|
||||||
|
|
||||||
def saveSSHAccessChanges(request):
|
def saveSSHAccessChanges(request):
|
||||||
try:
|
try:
|
||||||
userID = request.session['userID']
|
userID = request.session['userID']
|
||||||
@@ -1923,3 +1977,53 @@ def get_website_resources(request):
|
|||||||
except BaseException as msg:
|
except BaseException as msg:
|
||||||
logging.CyberCPLogFileWriter.writeToFile(f'Error in get_website_resources: {str(msg)}')
|
logging.CyberCPLogFileWriter.writeToFile(f'Error in get_website_resources: {str(msg)}')
|
||||||
return JsonResponse({'status': 0, 'error_message': 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:
|
else:
|
||||||
Data['ftp'] = 0
|
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)
|
proc = httpProc(request, 'websiteFunctions/website.html', Data)
|
||||||
return proc.render()
|
return proc.render()
|
||||||
else:
|
else:
|
||||||
@@ -4942,8 +4981,67 @@ StrictHostKeyChecking no
|
|||||||
website = Websites.objects.get(domain=self.domain)
|
website = Websites.objects.get(domain=self.domain)
|
||||||
externalApp = website.externalApp
|
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',
|
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()
|
return proc.render()
|
||||||
|
|
||||||
def saveSSHAccessChanges(self, userID=None, data=None):
|
def saveSSHAccessChanges(self, userID=None, data=None):
|
||||||
@@ -7259,3 +7357,5 @@ StrictHostKeyChecking no
|
|||||||
data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
|
data_ret = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)}
|
||||||
json_data = json.dumps(data_ret)
|
json_data = json.dumps(data_ret)
|
||||||
return HttpResponse(json_data)
|
return HttpResponse(json_data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user