· 6 years ago · Sep 02, 2019, 07:26 PM
1'use strict';
2
3/**
4 * Pterodactyl - Daemon
5 * Copyright (c) 2015 - 2018 Dane Everitt <dane@daneeveritt.com>.
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in all
15 * copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 * SOFTWARE.
24 */
25const rfr = require('rfr');
26const Async = require('async');
27const Request = require('request');
28const Util = require('util');
29const Fs = require('fs-extra');
30const Mime = require('mime');
31const Path = require('path');
32const Crypto = require('crypto');
33const _ = require('lodash');
34const Os = require('os');
35const Cache = require('memory-cache');
36const { exec } = require('child_process');
37
38const Status = require('./../helpers/status');
39const ConfigHelper = require('./../helpers/config');
40const ResponseHelper = require('./../helpers/responses');
41const BuilderController = require('./../controllers/builder');
42const DeleteController = require('./../controllers/delete');
43const Log = require('./../helpers/logger');
44const Package = require('./../../package');
45
46const Config = new ConfigHelper();
47
48class RouteController {
49 constructor(auth, req, res) {
50 this.req = req;
51 this.res = res;
52
53 this.auth = auth;
54 this.responses = new ResponseHelper(req, res);
55 }
56
57 // Returns Index
58 getIndex() {
59 this.auth.allowed('c:info', (allowedErr, isAllowed) => {
60 if (allowedErr || !isAllowed) return;
61
62 this.res.send({
63 name: 'Pterodactyl Management Daemon',
64 version: Package.version,
65 system: {
66 type: Os.type(),
67 arch: Os.arch(),
68 platform: Os.platform(),
69 release: Os.release(),
70 cpus: Os.cpus().length,
71 freemem: Os.freemem(),
72 },
73 network: Os.networkInterfaces(),
74 });
75 });
76 }
77
78 // Revoke an authentication key on demand
79 revokeKey() {
80 this.auth.allowed('c:revoke-key', (allowedErr, isAllowed) => {
81 if (allowedErr || !isAllowed) return;
82
83 const key = _.get(this.req.params, 'key');
84 Log.debug({ token: key }, 'Revoking authentication token per manual request.');
85 Cache.del(`auth:token:${key}`);
86
87 return this.responses.generic204(null);
88 });
89 }
90
91 // Similar to revokeKey except it allows for multiple keys at once
92 batchDeleteKeys() {
93 this.auth.allowed('c:revoke-key', (allowedErr, isAllowed) => {
94 if (allowedErr || !isAllowed) return;
95
96 _.forEach(_.get(this.req.params, 'keys'), key => {
97 Log.debug({ token: key }, 'Revoking authentication token per batch delete request.');
98 Cache.del(`auth:token:${key}`);
99 });
100
101 return this.responses.generic204(null);
102 });
103 }
104
105 // Updates saved configuration on system.
106 patchConfig() {
107 this.auth.allowed('c:config', (allowedErr, isAllowed) => {
108 if (allowedErr || !isAllowed) return;
109
110 Config.modify(this.req.params, err => {
111 this.responses.generic204(err);
112 });
113 });
114 }
115
116 // Saves Daemon Configuration to Disk
117 putConfig() {
118 this.auth.allowed('c:config', (allowedErr, isAllowed) => {
119 if (allowedErr || !isAllowed) return;
120
121 Config.save(this.req.params, err => {
122 this.responses.generic204(err);
123 });
124 });
125 }
126
127 postNewServer() {
128 this.auth.allowed('c:create', (allowedErr, isAllowed) => {
129 if (allowedErr || !isAllowed) return;
130
131 const startOnCompletion = _.get(this.req.params, 'start_on_completion', false);
132 if (startOnCompletion) {
133 delete this.req.params.start_on_completion;
134 }
135
136 const Builder = new BuilderController(this.req.params);
137 this.res.send(202, { 'message': 'Server is being built now, this might take some time if the docker image doesn\'t exist on the system yet.' });
138
139 // We sent a HTTP 202 since this might take awhile.
140 // We do need to monitor for errors and negatiate with
141 // the panel if they do occur.
142 Builder.init((err, data) => {
143 if (err) Log.fatal({ err: err, meta: _.get(err, 'meta') }, 'A fatal error was encountered while attempting to create a server.'); // eslint-disable-line
144
145 const HMAC = Crypto.createHmac('sha256', Config.get('keys.0'));
146 HMAC.update(data.uuid);
147
148 Request.post(`${Config.get('remote.base')}/daemon/install`, {
149 form: {
150 server: data.uuid,
151 signed: HMAC.digest('base64'),
152 installed: (err) ? 'error' : 'installed',
153 },
154 headers: {
155 'X-Access-Node': Config.get('keys.0'),
156 'Accept': 'application/json',
157 'Content-Type': 'application/json',
158 },
159 followAllRedirects: true,
160 timeout: 5000,
161 }, (requestErr, response, body) => {
162 if (requestErr || response.statusCode !== 200) {
163 Log.warn(requestErr, 'An error occured while attempting to alert the panel of server install status.', { code: (typeof response !== 'undefined') ? response.statusCode : null, responseBody: body });
164 } else {
165 Log.info('Notified remote panel of server install status.');
166 }
167
168 if (startOnCompletion && !err) {
169 const Servers = rfr('src/helpers/initialize.js').Servers;
170 Servers[data.uuid].start(startErr => {
171 if (err) Log.error({ server: data.uuid, err: startErr }, 'There was an error while attempting to auto-start this server.');
172 });
173 }
174 });
175 });
176 });
177 }
178
179 getAllServers() {
180 this.auth.allowed('c:list', (allowedErr, isAllowed) => {
181 if (allowedErr || !isAllowed) return;
182
183 const responseData = {};
184 Async.each(this.auth.allServers(), (server, callback) => {
185 responseData[server.json.uuid] = {
186 container: server.json.container,
187 service: server.json.service,
188 status: server.status,
189 query: server.processData.query,
190 proc: server.processData.process,
191 };
192 callback();
193 }, () => {
194 this.res.send(responseData);
195 });
196 });
197 }
198
199 deleteServer() {
200 this.auth.allowed('g:server:delete', (allowedErr, isAllowed) => {
201 if (allowedErr || !isAllowed) return;
202
203 const Delete = new DeleteController(this.auth.server().json);
204 Delete.delete(err => {
205 this.responses.generic204(err);
206 });
207 });
208 }
209
210 // Handles server power
211 putServerPower() {
212 if (this.req.params.action === 'start') {
213 this.auth.allowed('s:power:start', (allowedErr, isAllowed) => {
214 if (allowedErr || !isAllowed) return;
215
216 this.auth.server().start(err => {
217 if (err && (
218 _.includes(err.message, 'Server is currently queued for a container rebuild') ||
219 _.includes(err.message, 'Server container was not found and needs to be rebuilt.') ||
220 _.startsWith(err.message, 'Server is already running')
221 )) {
222 return this.res.send(202, { 'message': err.message });
223 }
224
225 this.responses.generic204(err);
226 });
227 });
228 } else if (this.req.params.action === 'stop') {
229 this.auth.allowed('s:power:stop', (allowedErr, isAllowed) => {
230 if (allowedErr || !isAllowed) return;
231
232 this.auth.server().stop(err => {
233 this.responses.generic204(err);
234 });
235 });
236 } else if (this.req.params.action === 'restart') {
237 this.auth.allowed('s:power:restart', (allowedErr, isAllowed) => {
238 if (allowedErr || !isAllowed) return;
239
240 this.auth.server().restart(err => {
241 if (err && (_.includes(err.message, 'Server is currently queued for a container rebuild') || _.includes(err.message, 'Server container was not found and needs to be rebuilt.'))) {
242 return this.res.send(202, { 'message': err.message });
243 }
244 this.responses.generic204(err);
245 });
246 });
247 } else if (this.req.params.action === 'kill') {
248 this.auth.allowed('s:power:kill', (allowedErr, isAllowed) => {
249 if (allowedErr || !isAllowed) return;
250
251 this.auth.server().kill(err => {
252 if (err && _.startsWith(err.message, 'Server is already stopped')) {
253 return this.res.send(202, { 'message': err.message });
254 }
255
256 this.responses.generic204(err);
257 });
258 });
259 } else {
260 this.res.send(404, { 'error': 'Unknown power action recieved.' });
261 }
262 }
263
264 reinstallServer() {
265 this.auth.allowed('c:install-server', (allowedErr, isAllowed) => {
266 if (allowedErr || !isAllowed) return;
267
268 this.auth.server().reinstall(this.req.params, err => {
269 if (err) Log.error(err);
270
271 const HMAC = Crypto.createHmac('sha256', Config.get('keys.0'));
272 HMAC.update(this.auth.serverUuid());
273
274 Request.post(`${Config.get('remote.base')}/daemon/install`, {
275 form: {
276 server: this.auth.serverUuid(),
277 signed: HMAC.digest('base64'),
278 installed: (err) ? 'error' : 'installed',
279 },
280 headers: {
281 'X-Access-Node': Config.get('keys.0'),
282 'Accept': 'application/json',
283 'Content-Type': 'application/json',
284 },
285 followAllRedirects: true,
286 timeout: 5000,
287 }, (requestErr, response, body) => {
288 if (requestErr || response.statusCode !== 200) {
289 Log.warn(requestErr, 'An error occured while attempting to alert the panel of server install status.', { code: (typeof response !== 'undefined') ? response.statusCode : null, responseBody: body });
290 } else {
291 Log.info('Notified remote panel of server install status.');
292 }
293 });
294 });
295
296 this.res.send(202, { 'message': 'Server is being reinstalled.' });
297 });
298 }
299
300 getServer() {
301 this.auth.allowed('s:console', (allowedErr, isAllowed) => {
302 if (allowedErr || !isAllowed) return;
303
304 this.res.send({
305 // container: this.auth.server().json.container,
306 // service: this.auth.server().json.service,
307 status: this.auth.server().status,
308 query: this.auth.server().processData.query,
309 proc: this.auth.server().processData.process,
310 });
311 });
312 }
313
314 // Sends command to server
315 postServerCommand() {
316 this.auth.allowed('s:command', (allowedErr, isAllowed) => {
317 if (allowedErr || !isAllowed) return;
318
319 if (this.auth.server().status === Status.OFF) {
320 return this.res.send(412, {
321 'error': 'Server is not running.',
322 'route': this.req.path,
323 'req_id': this.req.id,
324 'type': this.req.contentType,
325 });
326 }
327
328 if (!_.isUndefined(this.req.params.command)) {
329 this.auth.server().command(this.req.params.command).then(() => {
330 this.responses.generic204();
331 }).catch(err => {
332 this.responses.generic500(err);
333 });
334 } else {
335 this.res.send(500, { 'error': 'Missing command in request.' });
336 }
337 });
338 }
339
340 // Returns listing of server files.
341 getServerDirectory() {
342 this.auth.allowed('s:files:get', (allowedErr, isAllowed) => {
343 if (allowedErr || !isAllowed) return;
344
345 this.auth.server().fs.directory(this.req.params[0], (err, data) => {
346 if (err) {
347 switch (err.code) {
348 case 'ENOENT':
349 return this.res.send(404);
350 default:
351 return this.responses.generic500(err);
352 }
353 }
354 return this.res.send(data);
355 });
356 });
357 }
358
359 // Return file contents
360 getServerFile() {
361 this.auth.allowed('s:files:read', (allowedErr, isAllowed) => {
362 if (allowedErr || !isAllowed) return;
363
364 this.auth.server().fs.read(this.req.params[0], (err, data) => {
365 if (err) {
366 switch (err.code) {
367 case 'ENOENT':
368 return this.res.send(404);
369 default:
370 return this.responses.generic500(err);
371 }
372 }
373 return this.res.send({ content: data });
374 });
375 });
376 }
377
378 getServerLog() {
379 this.auth.allowed('s:console', (allowedErr, isAllowed) => {
380 if (allowedErr || !isAllowed) return;
381
382 this.auth.server().fs.readEnd(this.auth.server().service.object.log.location, (err, data) => {
383 if (err) {
384 return this.responses.generic500(err);
385 }
386 return this.res.send(data);
387 });
388 });
389 }
390
391 getServerFileStat() {
392 this.auth.allowed('s:files:read', (allowedErr, isAllowed) => {
393 if (allowedErr || !isAllowed) return;
394
395 this.auth.server().fs.stat(this.req.params[0], (err, data) => {
396 if (err) {
397 switch (err.code) {
398 case 'ENOENT':
399 return this.res.send(404);
400 default:
401 return this.responses.generic500(err);
402 }
403 }
404 return this.res.send(data);
405 });
406 });
407 }
408
409 postFileFolder() {
410 this.auth.allowed('s:files:create', (allowedErr, isAllowed) => {
411 if (allowedErr || !isAllowed) return;
412
413 this.auth.server().fs.mkdir(this.req.params.path, err => {
414 this.responses.generic204(err);
415 });
416 });
417 }
418
419 postFileCopy() {
420 this.auth.allowed('s:files:copy', (allowedErr, isAllowed) => {
421 if (allowedErr || !isAllowed) return;
422
423 this.auth.server().fs.copy(this.req.params.from, this.req.params.to, err => {
424 this.responses.generic204(err);
425 });
426 });
427 }
428
429 // prevent breaking API change for now.
430 deleteServerFile() {
431 this.auth.allowed('s:files:delete', (allowedErr, isAllowed) => {
432 if (allowedErr || !isAllowed) return;
433
434 this.auth.server().fs.rm(this.req.params[0], err => {
435 this.responses.generic204(err);
436 });
437 });
438 }
439
440 postFileDelete() {
441 this.auth.allowed('s:files:delete', (allowedErr, isAllowed) => {
442 if (allowedErr || !isAllowed) return;
443
444 this.auth.server().fs.rm(this.req.params.items, err => {
445 this.responses.generic204(err);
446 });
447 });
448 }
449
450 postFileMove() {
451 this.auth.allowed('s:files:move', (allowedErr, isAllowed) => {
452 if (allowedErr || !isAllowed) return;
453
454 this.auth.server().fs.move(this.req.params.from, this.req.params.to, err => {
455 this.responses.generic204(err);
456 });
457 });
458 }
459
460 postFileDecompress() {
461 this.auth.allowed('s:files:decompress', (allowedErr, isAllowed) => {
462 if (allowedErr || !isAllowed) return;
463
464 this.auth.server().fs.decompress(this.req.params.files, err => {
465 this.responses.generic204(err);
466 });
467 });
468 }
469
470 postFileCompress() {
471 this.auth.allowed('s:files:compress', (allowedErr, isAllowed) => {
472 if (allowedErr || !isAllowed) return;
473
474 this.auth.server().fs.compress(this.req.params.files, this.req.params.to, (err, filename) => {
475 if (err) {
476 return this.responses.generic500(err);
477 }
478 return this.res.send({
479 saved_as: filename,
480 });
481 });
482 });
483 }
484
485 postServerFile() {
486 this.auth.allowed('s:files:post', (allowedErr, isAllowed) => {
487 if (allowedErr || !isAllowed) return;
488
489 this.auth.server().fs.write(this.req.params.path, this.req.params.content, err => {
490 this.responses.generic204(err);
491 });
492 });
493 }
494
495 updateServerConfig() {
496 this.auth.allowed('g:server:patch', (allowedErr, isAllowed) => {
497 if (allowedErr || !isAllowed) return;
498
499 this.auth.server().modifyConfig(this.req.params, (this.req.method === 'PUT'), err => {
500 this.responses.generic204(err);
501 });
502 });
503 }
504
505 rebuildServer() {
506 this.auth.allowed('g:server:rebuild', (allowedErr, isAllowed) => {
507 if (allowedErr || !isAllowed) return;
508
509 this.auth.server().modifyConfig({ rebuild: true }, false, err => {
510 this.responses.generic204(err);
511 });
512 });
513 }
514
515 postServerSuspend() {
516 this.auth.allowed('g:server:suspend', (allowedErr, isAllowed) => {
517 if (allowedErr || !isAllowed) return;
518
519 this.auth.server().suspend(err => {
520 this.responses.generic204(err);
521 });
522 });
523 }
524
525 postServerUnsuspend() {
526 this.auth.allowed('g:server:unsuspend', (allowedErr, isAllowed) => {
527 if (allowedErr || !isAllowed) return;
528
529 this.auth.server().unsuspend(err => {
530 this.responses.generic204(err);
531 });
532 });
533 }
534
535 downloadServerFile() {
536 Request(`${Config.get('remote.base')}/api/remote/download-file`, {
537 method: 'POST',
538 json: {
539 token: this.req.params.token,
540 },
541 headers: {
542 'Accept': 'application/vnd.pterodactyl.v1+json',
543 'Authorization': `Bearer ${Config.get('keys.0')}`,
544 },
545 timeout: 5000,
546 }, (err, response, body) => {
547 if (err) {
548 Log.warn(err, 'Download action failed due to an error with the request.');
549 return this.res.send(500, { 'error': 'An error occured while attempting to perform this request.' });
550 }
551
552 if (response.statusCode === 200) {
553 try {
554 const json = _.isString(body) ? JSON.parse(body) : body;
555 if (!_.isUndefined(json) && json.path) {
556 const Server = this.auth.allServers();
557 // Does the server even exist?
558 if (_.isUndefined(Server[json.server])) {
559 return this.res.send(404, { 'error': 'No server found for the specified resource.' });
560 }
561
562 // Get necessary information for the download.
563 const Filename = Path.basename(json.path);
564 const Mimetype = Mime.getType(json.path);
565 const File = Server[json.server].path(json.path);
566 const Stat = Fs.statSync(File);
567 if (!Stat.isFile()) {
568 return this.res.send(404, { 'error': 'Could not locate the requested file.' });
569 }
570
571 this.res.writeHead(200, {
572 'Content-Type': Mimetype,
573 'Content-Length': Stat.size,
574 'Content-Disposition': Util.format('attachment; filename=%s', Filename),
575 });
576 const Filestream = Fs.createReadStream(File);
577 Filestream.pipe(this.res);
578 } else {
579 return this.res.send(424, { 'error': 'The upstream response did not include a valid download path.' });
580 }
581 } catch (ex) {
582 Log.error(ex);
583 return this.res.send(500, { 'error': 'An unexpected error occured while attempting to process this request.' });
584 }
585 } else {
586 if (response.statusCode >= 500) {
587 Log.warn({ res_code: response.statusCode, res_body: body }, 'An error occured while attempting to retrieve file download information for an upstream provider.');
588 }
589
590 this.res.redirect(this.req.header('Referer') || Config.get('remote.base'), _.constant(''));
591 }
592 });
593 }
594 getVersions() {
595 this.auth.allowed('s:version', (allowedErr, isAllowed) => {
596 if (allowedErr || !isAllowed) return;
597
598 let files = [];
599
600 Fs.readdirSync('/srv/minecraft-versions/').forEach(file => {
601 files.push(file);
602 });
603
604 this.res.send({"success": "true", "versions": files});
605 });
606 }
607
608 switchVersion() {
609 this.auth.allowed('s:version', (allowedErr, isAllowed) => {
610 if (allowedErr || !isAllowed) return;
611
612 if ("version" in this.req.params === false)
613 return this.res.send({"success": "false", "error": "Missing version argument"});
614
615 const version = this.req.params["version"];
616
617 const uuid = this.auth.server().uuid;
618
619 Fs.access('/srv/minecraft-versions/' + version + ".jar", error => {
620 if (!error) {
621 const res = this.res;
622
623 exec('cp /srv/minecraft-versions/' + version + ".jar /srv/daemon-data/" + uuid + "/server.jar && chown pterodactyl:pterodactyl /srv/daemon-data/" + uuid + "/server.jar", function(err, stdout, stderr) {
624 if (err) {
625 res.send({"success": "false", "error": "Server jar move error!"});
626 }
627
628 res.send({"success": "true"});
629 });
630 } else {
631 this.res.send({"success": "false", "error": "Version not found!"});
632 }
633 });
634 });
635 }
636
637 }
638
639}
640
641module.exports = RouteController;