-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
454 lines (397 loc) · 16.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
/*
* __ __ __
* _____/ /____ ______ ____ _____/ /_____/ /
* / ___/ //_/ / / / __ \/ __ \/ ___/ __/ __ /
* (__ ) ,< / /_/ / /_/ / /_/ / / / /_/ /_/ /
* /____/_/|_|\__, / .___/\____/_/ \__/\__,_/
* /____/_/
*
* Skyport Daemon v0.3.0 (Desiro City)
* (c) 2024 Matt James and contributers
*
*/
/**
* @fileoverview Main entry file for the Skyport Daemon. This module sets up an
* Express server integrated with Docker for container management and WebSocket for real-time communication.
* It includes routes for instance management, deployment, and power control, as well as WebSocket endpoints
* for real-time container stats and logs. Authentication is enforced using basic authentication.
*
* The server initializes with logging and configuration, sets up middleware for body parsing and authentication,
* and dynamically handles WebSocket connections for various operational commands and telemetry.
*/
process.env.dockerSocket = process.platform === "win32" ? "//./pipe/docker_engine" : "/var/run/docker.sock";
const express = require('express');
const Docker = require('dockerode');
const basicAuth = require('express-basic-auth');
const bodyParser = require('body-parser');
const CatLoggr = require('cat-loggr');
const WebSocket = require('ws');
const http = require('http');
const fs = require('node:fs');
const path = require('path');
const chalk = require('chalk')
const ascii = fs.readFileSync('./handlers/ascii.txt', 'utf8');
const { exec } = require('child_process');
const { init, createVolumesFolder } = require('./handlers/init.js');
const { seed } = require('./handlers/seed.js');
const { start, createNewVolume } = require('./routes/FTP.js')
const { createDatabaseAndUser } = require('./routes/Database.js');
const config = require('./config.json');
const docker = new Docker({ socketPath: process.env.dockerSocket });
/**
* Initializes a WebSocket server tied to the HTTP server. This WebSocket server handles real-time
* interactions such as authentication, container statistics reporting, logs streaming, and container
* control commands (start, stop, restart). The WebSocket server checks for authentication on connection
* and message reception, parsing messages as JSON and handling them according to their specified event type.
*/
const app = express();
const server = http.createServer(app);
const log = new CatLoggr();
/**
* Sets up Express application middleware for JSON body parsing and basic authentication using predefined
* user keys from the configuration. Initializes routes for managing Docker instances, deployments, and
* power controls. These routes are grouped under the '/instances' path.
*/
console.log(chalk.gray(ascii) + chalk.white(`version v${config.version}\n`));
init();
seed();
app.use(bodyParser.json());
app.use(basicAuth({
users: { 'Skyport': config.key },
challenge: true
}));
// FTP
start();
app.get('/ftp/info/:id', (req, res) => {
const filePath = './ftp/user-' + req.params.id + '.json';
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
res.status(500).json({ error: 'Error reading file' });
return;
}
res.json(JSON.parse(data));
});
});
// Databases
app.post('/database/create/:name', async (req, res) => {
try {
const dbName = req.params.name;
const credentials = await createDatabaseAndUser(dbName);
res.status(200).json({
message: `Database ${dbName} created successfully`,
credentials
});
} catch (error) {
console.error('Error creating database:', error);
res.status(500).json({ error: 'Failed to create database' });
}
});
// Function to dynamically load routers
function loadRouters() {
const routesDir = path.join(__dirname, 'routes');
fs.readdir(routesDir, (err, files) => {
if (err) {
log.error(`Error reading routes directory: ${err.message}`);
return;
}
files.forEach((file) => {
if (file.endsWith('.js')) {
try {
const routerPath = path.join(routesDir, file);
const router = require(routerPath);
if (typeof router === 'function' && router.name === 'router') {
const routeName = path.parse(file).name;
app.use(`/`, router);
log.info(`Loaded router: ${routeName}`);
} else {
log.warn(`File ${file} isn't a router. Not loading it`);
}
} catch (error) {
log.error(`Error loading router from ${file}: ${error.message}`);
}
}
});
});
}
// Call the function to load routers
loadRouters();
/**
* Initializes a WebSocket server tied to the HTTP server. This WebSocket server handles real-time
* interactions such as authentication, container statistics reporting, logs streaming, and container
* control commands (start, stop, restart). The WebSocket server checks for authentication on connection
* and message reception, parsing messages as JSON and handling them according to their specified event type.
*
* @param {http.Server} server - The HTTP server to bind the WebSocket server to.
*/
function initializeWebSocketServer(server) {
const wss = new WebSocket.Server({ server }); // use express-ws so you can have multiple ws's, api routes & that on 1 server.
wss.on('connection', (ws, req) => {
let isAuthenticated = false;
ws.on('message', async (message) => {
log.debug('got ' + message);
let msg = {};
try {
msg = JSON.parse(message);
} catch (error) {
ws.send('Invalid JSON');
return;
}
if (msg.event === 'auth' && msg.args) {
authenticateWebSocket(ws, req, msg.args[0], (authenticated, containerId, volumeId) => {
if (authenticated) {
isAuthenticated = true;
handleWebSocketConnection(ws, req, containerId, volumeId);
} else {
ws.send('Authentication failed');
ws.close(1008, "Authentication failed");
}
});
} else if (isAuthenticated) {
const urlParts = req.url.split('/');
const containerId = urlParts[2];
if (!containerId) {
ws.close(1008, "Container ID not specified");
return;
}
const container = docker.getContainer(containerId);
switch (msg.event) {
case 'cmd':
// Do absolutely fucking nothing. Not handling it here.
break;
case 'power:start':
performPowerAction(ws, container, 'start');
break;
case 'power:stop':
performPowerAction(ws, container, 'stop');
break;
case 'power:restart':
performPowerAction(ws, container, 'restart');
break;
default:
ws.send('Unsupported event');
break;
}
} else {
ws.send('Unauthorized access');
ws.close(1008, "Unauthorized access");
}
});
function authenticateWebSocket(ws, req, password, callback) {
if (password === config.key) {
log.info('successful authentication on ws');
ws.send(`\r\n\u001b[33m[skyportd] \x1b[0mconnected!\r\n`);
const urlParts = req.url.split('/');
const containerId = urlParts[2];
const volumeId = urlParts[3] || 0;
if (!containerId) {
ws.close(1008, "Container ID not specified");
callback(false, null);
return;
}
callback(true, containerId, volumeId);
} else {
log.warn('authentication failure on websocket!');
callback(false, null);
}
}
function handleWebSocketConnection(ws, req, containerId, volumeId) {
const container = docker.getContainer(containerId);
const volume = volumeId || 0;
container.inspect(async (err, data) => {
if (err) {
ws.send('Container not found');
return;
}
if (req.url.startsWith('/exec/')) {
setupExecSession(ws, container);
} else if (req.url.startsWith('/stats/')) {
setupStatsStreaming(ws, container, volume);
} else {
ws.close(1002, "URL must start with /exec/ or /stats/");
}
});
}
async function setupExecSession(ws, container) {
const logStream = await container.logs({
follow: true,
stdout: true,
stderr: true,
tail: 25
});
logStream.on('data', chunk => {
ws.send(chunk.toString());
});
ws.on('message', (msg) => {
if (isAuthenticated) {
const command = JSON.parse(msg).command;
if (command === "skyportCredits") {
ws.send("privt00, am5z, achul123, thatdevwolfy");
} else if (command) {
executeCommand(ws, container, command);
}
}
});
ws.on('close', () => {
logStream.destroy();
log.info('WebSocket client disconnected');
});
}
async function setupStatsStreaming(ws, container, volumeId) {
const fetchStats = async () => {
try {
const stats = await new Promise((resolve, reject) => {
container.stats({ stream: false }, (err, stats) => {
if (err) {
reject(new Error('Failed to fetch stats'));
} else {
resolve(stats);
}
});
});
// Calculate volume size
const volumeSize = await getVolumeSize(volumeId);
// Add volume size to stats object
stats.volumeSize = volumeSize;
ws.send(JSON.stringify(stats));
} catch (error) {
ws.send(JSON.stringify({ error: error.message }));
}
};
await fetchStats();
const statsInterval = setInterval(fetchStats, 2000);
ws.on('close', () => {
clearInterval(statsInterval);
log.info('WebSocket client disconnected');
});
}
async function executeCommand(ws, container, command) {
try {
const stream = await container.attach({
stream: true,
stdin: true,
stdout: true,
stderr: true,
hijack: true
});
stream.on('data', (chunk) => {
//ws.send(chunk.toString('utf8'));
});
stream.on('end', () => {
log.info('Attach stream ended');
ws.send('\nCommand execution completed');
});
stream.on('error', (err) => {
log.error('Attach stream error:', err);
ws.send(`Error in attach stream: ${err.message}`);
});
// Write the command to the stream
stream.write(command + '\n'); // your not disattaching.
} catch (err) {
log.error('Failed to attach to container:', err);
ws.send(`Failed to attach to container: ${err.message}`);
}
}
async function performPowerAction(ws, container, action) {
const actionMap = {
'start': container.start.bind(container),
'stop': container.kill.bind(container),
'restart': container.restart.bind(container),
};
if (!actionMap[action]) {
ws.send(`\r\n\u001b[33m[skyportd] \x1b[0Invalid action: ${action}\r\n`);
return;
}
ws.send(`\r\n\u001b[33m[skyportd] \x1b[0mWorking on ${action}...\r\n`);
try {
await actionMap[action]();
} catch (err) {
console.error(`Error performing ${action} action:`, err);
ws.send(`\r\n\u001b[33m[skyportd] \x1b[0Action failed: ${err.message}\r\n`);
}
}
async function getVolumeSize(volumeId) {
const volumePath = path.join('./volumes', volumeId);
try {
const totalSize = await calculateDirectorySize(volumePath);
return formatBytes(totalSize);
} catch (err) {
return 'Unknown';
}
}
function calculateDirectorySize(directoryPath, currentDepth) {
if (currentDepth >= 500) {
log.warn(`Maximum depth reached at ${directoryPath}`);
return 0;
}
let totalSize = 0;
const files = fs.readdirSync(directoryPath);
for (const file of files) {
const filePath = path.join(directoryPath, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
totalSize += calculateDirectorySize(filePath, currentDepth + 1);
} else {
totalSize += stats.size;
}
}
return totalSize;
}
// fixed in 0.2.2 sam
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
});
}
// Start the websocket server
initializeWebSocketServer(server);
/**
* Default HTTP GET route that provides basic daemon status information including Docker connectivity
* and system info. It performs a health check on Docker to ensure it's running and accessible, returning
* the daemon's status and any pertinent Docker system information in the response.
*/
app.get('/', async (req, res) => {
try {
const dockerInfo = await docker.info(); // Fetches information about the docker
const isDockerRunning = await docker.ping(); // Checks if the docker is up (which it probably is or this will err)
// Prepare the response object with Docker status
const response = {
versionFamily: 1,
versionRelease: 'skyportd ' + config.version,
online: true,
remote: config.remote,
mysql: {
host: config.mysql.host,
user: config.mysql.user,
password: config.mysql.password
},
docker: {
status: isDockerRunning ? 'running' : 'not running',
systemInfo: dockerInfo
}
};
res.json(response); // the point of this? just use the ws - yeah conn to the ws on nodes page and send that json over ws
} catch (error) {
console.error('Error fetching Docker status:', error);
res.status(500).json({ error: 'Docker is not running - skyportd will not function properly.' });
}
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something has... gone wrong!');
});
/**
* Starts the HTTP server with WebSocket support after a short delay, listening on the configured port.
* Logs a startup message indicating successful listening. This delayed start allows for any necessary
* initializations to complete before accepting incoming connections.
*/
const port = config.port;
setTimeout(function (){
server.listen(port, () => {
log.info('skyportd is listening on port ' + port);
});
}, 2000);