Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions src/deb/web-terminal/server.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions src/deb/web-terminal/web-terminal-session-auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/local/hestia/php/bin/php
<?php
declare(strict_types=1);

function deny(string $error, int $code = 1): never {
echo json_encode(
["ok" => false, "error" => $error],
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
),
PHP_EOL;
exit($code);
}

if (!isset($argv[1]) || !is_string($argv[1])) {
deny("missing session id");
}

$sessionId = $argv[1];
if ($sessionId === "" || preg_match('/^[A-Za-z0-9,-]+$/', $sessionId) !== 1) {
deny("invalid session id");
}

$hestia = getenv("HESTIA");
if (!is_string($hestia) || $hestia === "") {
deny("missing HESTIA env");
}

session_name("HESTIASID");
session_save_path($hestia . "/data/sessions");
session_id($sessionId);

if (!@session_start()) {
deny("session start failed");
}

$user = $_SESSION["user"] ?? "";
$look = $_SESSION["look"] ?? "";

if (!is_string($user) || $user === "") {
deny("unauthenticated");
}

if (!is_string($look)) {
$look = "";
}

echo json_encode(
["ok" => true, "user" => $user, "look" => $look],
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
),
PHP_EOL;

2 changes: 2 additions & 0 deletions src/hst_autocompile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,9 @@ if [ "$WEB_TERMINAL_B" = true ]; then
get_branch_file 'src/deb/web-terminal/package.json' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/package.json"
get_branch_file 'src/deb/web-terminal/package-lock.json' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/package-lock.json"
get_branch_file 'src/deb/web-terminal/server.js' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/server.js"
get_branch_file 'src/deb/web-terminal/web-terminal-session-auth.php' "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/web-terminal-session-auth.php"
chmod +x "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/server.js"
chmod +x "${BUILD_DIR_HESTIA_TERMINAL}/usr/local/hestia/web-terminal/web-terminal-session-auth.php"

cd $BUILD_DIR_HESTIA_TERMINAL/usr/local/hestia/web-terminal
npm ci --omit=dev
Expand Down