|
|
@@ -1,6 +1,6 @@
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
-import { execSync } from 'node:child_process';
|
|
|
+import { execFileSync, execSync } from 'node:child_process';
|
|
|
import { readFileSync } from 'node:fs';
|
|
|
import { spawn } from 'node-pty';
|
|
|
import { WebSocketServer } from 'ws';
|
|
|
@@ -14,10 +14,48 @@ const { config } = JSON.parse(
|
|
|
execSync(`${process.env.HESTIA}/bin/v-list-sys-config json`, { silent: true }).toString(),
|
|
|
);
|
|
|
|
|
|
+function parseCookies(cookieHeader) {
|
|
|
+ const cookies = {};
|
|
|
+ if (typeof cookieHeader !== 'string' || cookieHeader.length === 0) {
|
|
|
+ return cookies;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const part of cookieHeader.split(';')) {
|
|
|
+ const cookie = part.trim();
|
|
|
+ if (cookie.length === 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const separatorIndex = cookie.indexOf('=');
|
|
|
+ if (separatorIndex < 0) {
|
|
|
+ cookies[cookie] = cookies[cookie] || [];
|
|
|
+ cookies[cookie].push('');
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (separatorIndex === 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const key = cookie.slice(0, separatorIndex).trim();
|
|
|
+ const value = cookie.slice(separatorIndex + 1).trim();
|
|
|
+ if (key.length === 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ cookies[key] = cookies[key] || [];
|
|
|
+ cookies[key].push(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ return cookies;
|
|
|
+}
|
|
|
+
|
|
|
const wss = new WebSocketServer({
|
|
|
port: Number.parseInt(config.WEB_TERMINAL_PORT, 10),
|
|
|
verifyClient: async (info, cb) => {
|
|
|
- if (!info.req.headers.cookie.includes(sessionName)) {
|
|
|
+ const cookies = parseCookies(info.req.headers.cookie);
|
|
|
+ const sessionIDs = cookies[sessionName] || [];
|
|
|
+ if (sessionIDs.length !== 1 || sessionIDs[0].length === 0) {
|
|
|
cb(false, 401, 'Unauthorized');
|
|
|
return;
|
|
|
}
|
|
|
@@ -48,20 +86,37 @@ wss.on('connection', (ws, req) => {
|
|
|
const remoteIP = req.headers['x-real-ip'] || req.socket.remoteAddress;
|
|
|
|
|
|
// Check if session is valid
|
|
|
- const sessionID = req.headers.cookie.split(`${sessionName}=`)[1].split(';')[0];
|
|
|
+ const cookies = parseCookies(req.headers.cookie);
|
|
|
+ const sessionIDs = cookies[sessionName] || [];
|
|
|
+ if (sessionIDs.length !== 1 || sessionIDs[0].length === 0) {
|
|
|
+ console.error(`Missing ${sessionName} cookie from ${remoteIP}, refusing connection`);
|
|
|
+ ws.close(1000, 'You are not authenticated.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const sessionID = sessionIDs[0];
|
|
|
console.log(`New connection from ${remoteIP} (${sessionID})`);
|
|
|
|
|
|
- const file = readFileSync(`${process.env.HESTIA}/data/sessions/sess_${sessionID}`);
|
|
|
- if (!file) {
|
|
|
- console.error(`Invalid session ID ${sessionID}, refusing connection`);
|
|
|
+ let authResult;
|
|
|
+ try {
|
|
|
+ const raw = execFileSync(
|
|
|
+ `${process.env.HESTIA}/php/bin/php`,
|
|
|
+ [`${process.env.HESTIA}/web-terminal/web-terminal-session-auth.php`, sessionID],
|
|
|
+ { encoding: 'utf8' },
|
|
|
+ );
|
|
|
+ authResult = JSON.parse(raw);
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`Session helper failed for ${sessionID}, refusing connection: ${error.message}`);
|
|
|
ws.close(1000, 'Your session has expired.');
|
|
|
return;
|
|
|
}
|
|
|
- const session = file.toString();
|
|
|
|
|
|
- // Get username
|
|
|
- const login = session.split('user|s:')[1].split('"')[1];
|
|
|
- const impersonating = session.split('look|s:')[1].split('"')[1];
|
|
|
+ if (!authResult?.ok || typeof authResult.user !== 'string' || authResult.user.length === 0) {
|
|
|
+ console.error(`Unauthenticated session ${sessionID}, refusing connection`);
|
|
|
+ ws.close(1000, 'You are not authenticated.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const login = authResult.user;
|
|
|
+ const impersonating = typeof authResult.look === 'string' ? authResult.look : '';
|
|
|
const username = impersonating.length > 0 ? impersonating : login;
|
|
|
|
|
|
// Get user info
|