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
This commit is contained in:
@@ -131,6 +131,8 @@ Thank you so much!
|
|||||||
<a href="https://arvensis.systems/?utm_source=coolify.io"><img width="60px" alt="Arvensis Systems" src="https://coolify.io/images/arvensis.png"/></a>
|
<a href="https://arvensis.systems/?utm_source=coolify.io"><img width="60px" alt="Arvensis Systems" src="https://coolify.io/images/arvensis.png"/></a>
|
||||||
<a href="https://github.com/Niki2k1"><img width="60px" alt="Niklas Lausch" src="https://github.com/Niki2k1.png"/></a>
|
<a href="https://github.com/Niki2k1"><img width="60px" alt="Niklas Lausch" src="https://github.com/Niki2k1.png"/></a>
|
||||||
<a href="https://capgo.app/?utm_source=coolify.io"><img width="60px" alt="Cap-go" src="https://github.com/cap-go.png"/></a>
|
<a href="https://capgo.app/?utm_source=coolify.io"><img width="60px" alt="Cap-go" src="https://github.com/cap-go.png"/></a>
|
||||||
|
<a href="https://interviewpal.com/?utm_source=coolify.io"><img width="60px" alt="InterviewPal" src="/public/svgs/interviewpal.svg"/></a>
|
||||||
|
|
||||||
|
|
||||||
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
|
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
|
||||||
|
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
'version' => '4.0.0-beta.412',
|
'version' => '4.0.0-beta.413',
|
||||||
'helper_version' => '1.0.8',
|
'helper_version' => '1.0.8',
|
||||||
'realtime_version' => '1.0.7',
|
'realtime_version' => '1.0.8',
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
'autoupdate' => env('AUTOUPDATE'),
|
'autoupdate' => env('AUTOUPDATE'),
|
||||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||||
|
@@ -3,7 +3,9 @@ import http from 'http';
|
|||||||
import pty from 'node-pty';
|
import pty from 'node-pty';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cookie from 'cookie';
|
import cookie from 'cookie';
|
||||||
import 'dotenv/config'
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
const userSessions = new Map();
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.url === '/ready') {
|
if (req.url === '/ready') {
|
||||||
@@ -15,16 +17,20 @@ const server = http.createServer((req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const verifyClient = async (info, callback) => {
|
const getSessionCookie = (req) => {
|
||||||
const cookies = cookie.parse(info.req.headers.cookie || '');
|
const cookies = cookie.parse(req.headers.cookie || '');
|
||||||
// const origin = new URL(info.origin);
|
|
||||||
// const protocol = origin.protocol;
|
|
||||||
const xsrfToken = cookies['XSRF-TOKEN'];
|
const xsrfToken = cookies['XSRF-TOKEN'];
|
||||||
|
|
||||||
// Generate session cookie name based on APP_NAME
|
|
||||||
const appName = process.env.APP_NAME || 'laravel';
|
const appName = process.env.APP_NAME || 'laravel';
|
||||||
const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`;
|
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
|
// Verify presence of required tokens
|
||||||
if (!laravelSession || !xsrfToken) {
|
if (!laravelSession || !xsrfToken) {
|
||||||
@@ -54,11 +60,24 @@ const verifyClient = async (info, callback) => {
|
|||||||
|
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server, path: '/terminal/ws', verifyClient: verifyClient });
|
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 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);
|
userSessions.set(userId, userSession);
|
||||||
|
|
||||||
ws.on('message', (message) => {
|
ws.on('message', (message) => {
|
||||||
@@ -125,6 +144,20 @@ async function handleCommand(ws, command, userId) {
|
|||||||
const timeout = extractTimeout(commandString);
|
const timeout = extractTimeout(commandString);
|
||||||
const sshArgs = extractSshArgs(commandString);
|
const sshArgs = extractSshArgs(commandString);
|
||||||
const hereDocContent = extractHereDocContent(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 = {
|
const options = {
|
||||||
name: 'xterm-color',
|
name: 'xterm-color',
|
||||||
cols: 80,
|
cols: 80,
|
||||||
@@ -152,7 +185,6 @@ async function handleCommand(ws, command, userId) {
|
|||||||
console.error(`Process exited with code ${exitCode} and signal ${signal}`);
|
console.error(`Process exited with code ${exitCode} and signal ${signal}`);
|
||||||
ws.send('pty-exited');
|
ws.send('pty-exited');
|
||||||
userSession.isActive = false;
|
userSession.isActive = false;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (timeout) {
|
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) {
|
async function handleError(err, userId) {
|
||||||
console.error('WebSocket error:', err);
|
console.error('WebSocket error:', err);
|
||||||
await killPtyProcess(userId);
|
await killPtyProcess(userId);
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.412"
|
"version": "4.0.0-beta.413"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.413"
|
"version": "4.0.0-beta.414"
|
||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.8"
|
"version": "1.0.8"
|
||||||
},
|
},
|
||||||
"realtime": {
|
"realtime": {
|
||||||
"version": "1.0.7"
|
"version": "1.0.8"
|
||||||
},
|
},
|
||||||
"sentinel": {
|
"sentinel": {
|
||||||
"version": "0.0.15"
|
"version": "0.0.15"
|
||||||
|
1
public/svgs/interviewpal.svg
Normal file
1
public/svgs/interviewpal.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.7 KiB |
@@ -149,6 +149,17 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
return response()->json(['authenticated' => false], 401);
|
return response()->json(['authenticated' => false], 401);
|
||||||
})->name('terminal.auth');
|
})->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::prefix('invitations')->group(function () {
|
||||||
Route::get('/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept');
|
Route::get('/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept');
|
||||||
Route::get('/{uuid}/revoke', [Controller::class, 'revoke_invitation'])->name('team.invitation.revoke');
|
Route::get('/{uuid}/revoke', [Controller::class, 'revoke_invitation'])->name('team.invitation.revoke');
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.412"
|
"version": "4.0.0-beta.413"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.413"
|
"version": "4.0.0-beta.414"
|
||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.8"
|
"version": "1.0.8"
|
||||||
},
|
},
|
||||||
"realtime": {
|
"realtime": {
|
||||||
"version": "1.0.7"
|
"version": "1.0.8"
|
||||||
},
|
},
|
||||||
"sentinel": {
|
"sentinel": {
|
||||||
"version": "0.0.15"
|
"version": "0.0.15"
|
||||||
|
Reference in New Issue
Block a user