From 0c0bdbf2fe21875e8804f2e23e2a3c2295187dba Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 28 Apr 2025 10:45:02 +0200
Subject: [PATCH] v4.0.0-beta.413 (#5711)
* feat(README): add InterviewPal sponsorship link and corresponding SVG icon
* chore(versions): update coolify version to 4.0.0-beta.413 and nightly version to 4.0.0-beta.414 in configuration files
* fix(terminal): enhance WebSocket client verification with authorized IPs in terminal server
* chore(versions): update realtime version to 1.0.8 in versions.json
* chore(versions): update realtime version to 1.0.8 in versions.json
---
README.md | 2 +
config/constants.php | 4 +-
docker/coolify-realtime/terminal-server.js | 72 ++++++++++++++++++----
other/nightly/versions.json | 6 +-
public/svgs/interviewpal.svg | 1 +
routes/web.php | 11 ++++
versions.json | 6 +-
7 files changed, 82 insertions(+), 20 deletions(-)
create mode 100644 public/svgs/interviewpal.svg
diff --git a/README.md b/README.md
index 6e245aa22..5f583df75 100644
--- a/README.md
+++ b/README.md
@@ -131,6 +131,8 @@ Thank you so much!
+
+
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
diff --git a/config/constants.php b/config/constants.php
index f05bc7d8e..a849ae93e 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,9 +2,9 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.412',
+ 'version' => '4.0.0-beta.413',
'helper_version' => '1.0.8',
- 'realtime_version' => '1.0.7',
+ 'realtime_version' => '1.0.8',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js
index 6649f866c..61dd29453 100755
--- a/docker/coolify-realtime/terminal-server.js
+++ b/docker/coolify-realtime/terminal-server.js
@@ -3,7 +3,9 @@ import http from 'http';
import pty from 'node-pty';
import axios from 'axios';
import cookie from 'cookie';
-import 'dotenv/config'
+import 'dotenv/config';
+
+const userSessions = new Map();
const server = http.createServer((req, res) => {
if (req.url === '/ready') {
@@ -15,16 +17,20 @@ const server = http.createServer((req, res) => {
}
});
-const verifyClient = async (info, callback) => {
- const cookies = cookie.parse(info.req.headers.cookie || '');
- // const origin = new URL(info.origin);
- // const protocol = origin.protocol;
+const getSessionCookie = (req) => {
+ const cookies = cookie.parse(req.headers.cookie || '');
const xsrfToken = cookies['XSRF-TOKEN'];
-
- // Generate session cookie name based on APP_NAME
const appName = process.env.APP_NAME || 'laravel';
const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`;
- const laravelSession = cookies[sessionCookieName];
+ return {
+ sessionCookieName,
+ xsrfToken: xsrfToken,
+ laravelSession: cookies[sessionCookieName]
+ }
+}
+
+const verifyClient = async (info, callback) => {
+ const { xsrfToken, laravelSession, sessionCookieName } = getSessionCookie(info.req);
// Verify presence of required tokens
if (!laravelSession || !xsrfToken) {
@@ -54,11 +60,24 @@ const verifyClient = async (info, callback) => {
const wss = new WebSocketServer({ server, path: '/terminal/ws', verifyClient: verifyClient });
-const userSessions = new Map();
-wss.on('connection', (ws) => {
+wss.on('connection', async (ws, req) => {
const userId = generateUserId();
- const userSession = { ws, userId, ptyProcess: null, isActive: false };
+ const userSession = { ws, userId, ptyProcess: null, isActive: false, authorizedIPs: [] };
+ const { xsrfToken, laravelSession, sessionCookieName } = getSessionCookie(req);
+
+ // Verify presence of required tokens
+ if (!laravelSession || !xsrfToken) {
+ ws.close(401, 'Unauthorized: Missing required tokens');
+ return;
+ }
+ const response = await axios.post(`http://coolify:8080/terminal/auth/ips`, null, {
+ headers: {
+ 'Cookie': `${sessionCookieName}=${laravelSession}`,
+ 'X-XSRF-TOKEN': xsrfToken
+ },
+ });
+ userSession.authorizedIPs = response.data.ipAddresses || [];
userSessions.set(userId, userSession);
ws.on('message', (message) => {
@@ -125,6 +144,20 @@ async function handleCommand(ws, command, userId) {
const timeout = extractTimeout(commandString);
const sshArgs = extractSshArgs(commandString);
const hereDocContent = extractHereDocContent(commandString);
+
+ // Extract target host from SSH command
+ const targetHost = extractTargetHost(sshArgs);
+ if (!targetHost) {
+ ws.send('Invalid SSH command: No target host found');
+ return;
+ }
+
+ // Validate target host against authorized IPs
+ if (!userSession.authorizedIPs.includes(targetHost)) {
+ ws.send(`Unauthorized: Target host ${targetHost} not in authorized list`);
+ return;
+ }
+
const options = {
name: 'xterm-color',
cols: 80,
@@ -152,7 +185,6 @@ async function handleCommand(ws, command, userId) {
console.error(`Process exited with code ${exitCode} and signal ${signal}`);
ws.send('pty-exited');
userSession.isActive = false;
-
});
if (timeout) {
@@ -162,6 +194,22 @@ async function handleCommand(ws, command, userId) {
}
}
+function extractTargetHost(sshArgs) {
+ // Find the argument that matches the pattern user@host
+ const userAtHost = sshArgs.find(arg => {
+ // Skip paths that contain 'storage/app/ssh/keys/'
+ if (arg.includes('storage/app/ssh/keys/')) {
+ return false;
+ }
+ return /^[^@]+@[^@]+$/.test(arg);
+ });
+ if (!userAtHost) return null;
+
+ // Extract host from user@host
+ const host = userAtHost.split('@')[1];
+ return host;
+}
+
async function handleError(err, userId) {
console.error('WebSocket error:', err);
await killPtyProcess(userId);
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index c3f4ba912..6dc9e7a02 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,16 +1,16 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.412"
+ "version": "4.0.0-beta.413"
},
"nightly": {
- "version": "4.0.0-beta.413"
+ "version": "4.0.0-beta.414"
},
"helper": {
"version": "1.0.8"
},
"realtime": {
- "version": "1.0.7"
+ "version": "1.0.8"
},
"sentinel": {
"version": "0.0.15"
diff --git a/public/svgs/interviewpal.svg b/public/svgs/interviewpal.svg
new file mode 100644
index 000000000..f0dc3731a
--- /dev/null
+++ b/public/svgs/interviewpal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/routes/web.php b/routes/web.php
index 3edfec910..3ddd77361 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -149,6 +149,17 @@ Route::middleware(['auth', 'verified'])->group(function () {
return response()->json(['authenticated' => false], 401);
})->name('terminal.auth');
+ Route::post('/terminal/auth/ips', function () {
+ if (auth()->check()) {
+ $team = auth()->user()->currentTeam();
+ $ipAddresses = $team->servers()->pluck('ip')->toArray();
+
+ return response()->json(['ipAddresses' => $ipAddresses], 200);
+ }
+
+ return response()->json(['ipAddresses' => []], 401);
+ })->name('terminal.auth.ips');
+
Route::prefix('invitations')->group(function () {
Route::get('/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept');
Route::get('/{uuid}/revoke', [Controller::class, 'revoke_invitation'])->name('team.invitation.revoke');
diff --git a/versions.json b/versions.json
index c3f4ba912..6dc9e7a02 100644
--- a/versions.json
+++ b/versions.json
@@ -1,16 +1,16 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.412"
+ "version": "4.0.0-beta.413"
},
"nightly": {
- "version": "4.0.0-beta.413"
+ "version": "4.0.0-beta.414"
},
"helper": {
"version": "1.0.8"
},
"realtime": {
- "version": "1.0.7"
+ "version": "1.0.8"
},
"sentinel": {
"version": "0.0.15"