Browse Source

web-terminal: use php helper for session auth lookup (#5244)

* web-terminal: use php helper for session auth lookup

* nit

* Harde cookie parsing

requested by https://github.com/numanturle
divinity76 1 week ago
parent
commit
854d71b3c1

+ 65 - 10
src/deb/web-terminal/server.js

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

+ 52 - 0
src/deb/web-terminal/web-terminal-session-auth.php

@@ -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 - 0
src/hst_autocompile.sh

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