· 6 years ago · Sep 03, 2019, 06:36 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');
37const http = require("http");
38
39
40const Status = require('./../helpers/status');
41const ConfigHelper = require('./../helpers/config');
42const ResponseHelper = require('./../helpers/responses');
43const BuilderController = require('./../controllers/builder');
44const DeleteController = require('./../controllers/delete');
45const Log = require('./../helpers/logger');
46const Package = require('./../../package');
47
48const Config = new ConfigHelper();
49
50class RouteController {
51 constructor(auth, req, res) {
52 this.req = req;
53 this.res = res;
54
55 this.auth = auth;
56 this.responses = new ResponseHelper(req, res);
57 }
58
59 // Returns Index
60 getIndex() {
61 this.auth.allowed('c:info', (allowedErr, isAllowed) => {
62 if (allowedErr || !isAllowed) return;
63
64 this.res.send({
65 name: 'Pterodactyl Management Daemon',
66 version: Package.version,
67 system: {
68 type: Os.type(),
69 arch: Os.arch(),
70 platform: Os.platform(),
71 release: Os.release(),
72 cpus: Os.cpus().length,
73 freemem: Os.freemem(),
74 },
75 network: Os.networkInterfaces(),
76 });
77 });
78 }
79
80 // Revoke an authentication key on demand
81 revokeKey() {
82 this.auth.allowed('c:revoke-key', (allowedErr, isAllowed) => {
83 if (allowedErr || !isAllowed) return;
84
85 const key = _.get(this.req.params, 'key');
86 Log.debug({ token: key }, 'Revoking authentication token per manual request.');
87 Cache.del(`auth:token:${key}`);
88
89 return this.responses.generic204(null);
90 });
91 }
92
93 // Similar to revokeKey except it allows for multiple keys at once
94 batchDeleteKeys() {
95 this.auth.allowed('c:revoke-key', (allowedErr, isAllowed) => {
96 if (allowedErr || !isAllowed) return;
97
98 _.forEach(_.get(this.req.params, 'keys'), key => {
99 Log.debug({ token: key }, 'Revoking authentication token per batch delete request.');
100 Cache.del(`auth:token:${key}`);
101 });
102
103 return this.responses.generic204(null);
104 });
105 }
106
107 // Updates saved configuration on system.
108 patchConfig() {
109 this.auth.allowed('c:config', (allowedErr, isAllowed) => {
110 if (allowedErr || !isAllowed) return;
111
112 Config.modify(this.req.params, err => {
113 this.responses.generic204(err);
114 });
115 });
116 }
117
118 // Saves Daemon Configuration to Disk
119 putConfig() {
120 this.auth.allowed('c:config', (allowedErr, isAllowed) => {
121 if (allowedErr || !isAllowed) return;
122
123 Config.save(this.req.params, err => {
124 this.responses.generic204(err);
125 });
126 });
127 }
128
129 postNewServer() {
130 this.auth.allowed('c:create', (allowedErr, isAllowed) => {
131 if (allowedErr || !isAllowed) return;
132
133 const startOnCompletion = _.get(this.req.params, 'start_on_completion', false);
134 if (startOnCompletion) {
135 delete this.req.params.start_on_completion;
136 }
137
138 const Builder = new BuilderController(this.req.params);
139 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.' });
140
141 // We sent a HTTP 202 since this might take awhile.
142 // We do need to monitor for errors and negatiate with
143 // the panel if they do occur.
144 Builder.init((err, data) => {
145 if (err) Log.fatal({ err: err, meta: _.get(err, 'meta') }, 'A fatal error was encountered while attempting to create a server.'); // eslint-disable-line
146
147 const HMAC = Crypto.createHmac('sha256', Config.get('keys.0'));
148 HMAC.update(data.uuid);
149
150 Request.post(`${Config.get('remote.base')}/daemon/install`, {
151 form: {
152 server: data.uuid,
153 signed: HMAC.digest('base64'),
154 installed: (err) ? 'error' : 'installed',
155 },
156 headers: {
157 'X-Access-Node': Config.get('keys.0'),
158 'Accept': 'application/json',
159 'Content-Type': 'application/json',
160 },
161 followAllRedirects: true,
162 timeout: 5000,
163 }, (requestErr, response, body) => {
164 if (requestErr || response.statusCode !== 200) {
165 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 });
166 } else {
167 Log.info('Notified remote panel of server install status.');
168 }
169
170 if (startOnCompletion && !err) {
171 const Servers = rfr('src/helpers/initialize.js').Servers;
172 Servers[data.uuid].start(startErr => {
173 if (err) Log.error({ server: data.uuid, err: startErr }, 'There was an error while attempting to auto-start this server.');
174 });
175 }
176 });
177 });
178 });
179 }
180
181 getAllServers() {
182 this.auth.allowed('c:list', (allowedErr, isAllowed) => {
183 if (allowedErr || !isAllowed) return;
184
185 const responseData = {};
186 Async.each(this.auth.allServers(), (server, callback) => {
187 responseData[server.json.uuid] = {
188 container: server.json.container,
189 service: server.json.service,
190 status: server.status,
191 query: server.processData.query,
192 proc: server.processData.process,
193 };
194 callback();
195 }, () => {
196 this.res.send(responseData);
197 });
198 });
199 }
200
201 deleteServer() {
202 this.auth.allowed('g:server:delete', (allowedErr, isAllowed) => {
203 if (allowedErr || !isAllowed) return;
204
205 const Delete = new DeleteController(this.auth.server().json);
206 Delete.delete(err => {
207 this.responses.generic204(err);
208 });
209 });
210 }
211
212 // Handles server power
213 putServerPower() {
214 if (this.req.params.action === 'start') {
215 this.auth.allowed('s:power:start', (allowedErr, isAllowed) => {
216 if (allowedErr || !isAllowed) return;
217
218 this.auth.server().start(err => {
219 if (err && (
220 _.includes(err.message, 'Server is currently queued for a container rebuild') ||
221 _.includes(err.message, 'Server container was not found and needs to be rebuilt.') ||
222 _.startsWith(err.message, 'Server is already running')
223 )) {
224 return this.res.send(202, { 'message': err.message });
225 }
226
227 this.responses.generic204(err);
228 });
229 });
230 } else if (this.req.params.action === 'stop') {
231 this.auth.allowed('s:power:stop', (allowedErr, isAllowed) => {
232 if (allowedErr || !isAllowed) return;
233
234 this.auth.server().stop(err => {
235 this.responses.generic204(err);
236 });
237 });
238 } else if (this.req.params.action === 'restart') {
239 this.auth.allowed('s:power:restart', (allowedErr, isAllowed) => {
240 if (allowedErr || !isAllowed) return;
241
242 this.auth.server().restart(err => {
243 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.'))) {
244 return this.res.send(202, { 'message': err.message });
245 }
246 this.responses.generic204(err);
247 });
248 });
249 } else if (this.req.params.action === 'kill') {
250 this.auth.allowed('s:power:kill', (allowedErr, isAllowed) => {
251 if (allowedErr || !isAllowed) return;
252
253 this.auth.server().kill(err => {
254 if (err && _.startsWith(err.message, 'Server is already stopped')) {
255 return this.res.send(202, { 'message': err.message });
256 }
257
258 this.responses.generic204(err);
259 });
260 });
261 } else {
262 this.res.send(404, { 'error': 'Unknown power action recieved.' });
263 }
264 }
265
266 reinstallServer() {
267 this.auth.allowed('c:install-server', (allowedErr, isAllowed) => {
268 if (allowedErr || !isAllowed) return;
269
270 this.auth.server().reinstall(this.req.params, err => {
271 if (err) Log.error(err);
272
273 const HMAC = Crypto.createHmac('sha256', Config.get('keys.0'));
274 HMAC.update(this.auth.serverUuid());
275
276 Request.post(`${Config.get('remote.base')}/daemon/install`, {
277 form: {
278 server: this.auth.serverUuid(),
279 signed: HMAC.digest('base64'),
280 installed: (err) ? 'error' : 'installed',
281 },
282 headers: {
283 'X-Access-Node': Config.get('keys.0'),
284 'Accept': 'application/json',
285 'Content-Type': 'application/json',
286 },
287 followAllRedirects: true,
288 timeout: 5000,
289 }, (requestErr, response, body) => {
290 if (requestErr || response.statusCode !== 200) {
291 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 });
292 } else {
293 Log.info('Notified remote panel of server install status.');
294 }
295 });
296 });
297
298 this.res.send(202, { 'message': 'Server is being reinstalled.' });
299 });
300 }
301
302 getServer() {
303 this.auth.allowed('s:console', (allowedErr, isAllowed) => {
304 if (allowedErr || !isAllowed) return;
305
306 this.res.send({
307 // container: this.auth.server().json.container,
308 // service: this.auth.server().json.service,
309 status: this.auth.server().status,
310 query: this.auth.server().processData.query,
311 proc: this.auth.server().processData.process,
312 });
313 });
314 }
315
316 // Sends command to server
317 postServerCommand() {
318 this.auth.allowed('s:command', (allowedErr, isAllowed) => {
319 if (allowedErr || !isAllowed) return;
320
321 if (this.auth.server().status === Status.OFF) {
322 return this.res.send(412, {
323 'error': 'Server is not running.',
324 'route': this.req.path,
325 'req_id': this.req.id,
326 'type': this.req.contentType,
327 });
328 }
329
330 if (!_.isUndefined(this.req.params.command)) {
331 this.auth.server().command(this.req.params.command).then(() => {
332 this.responses.generic204();
333 }).catch(err => {
334 this.responses.generic500(err);
335 });
336 } else {
337 this.res.send(500, { 'error': 'Missing command in request.' });
338 }
339 });
340 }
341
342 // Returns listing of server files.
343 getServerDirectory() {
344 this.auth.allowed('s:files:get', (allowedErr, isAllowed) => {
345 if (allowedErr || !isAllowed) return;
346
347 this.auth.server().fs.directory(this.req.params[0], (err, data) => {
348 if (err) {
349 switch (err.code) {
350 case 'ENOENT':
351 return this.res.send(404);
352 default:
353 return this.responses.generic500(err);
354 }
355 }
356 return this.res.send(data);
357 });
358 });
359 }
360
361 // Return file contents
362 getServerFile() {
363 this.auth.allowed('s:files:read', (allowedErr, isAllowed) => {
364 if (allowedErr || !isAllowed) return;
365
366 this.auth.server().fs.read(this.req.params[0], (err, data) => {
367 if (err) {
368 switch (err.code) {
369 case 'ENOENT':
370 return this.res.send(404);
371 default:
372 return this.responses.generic500(err);
373 }
374 }
375 return this.res.send({ content: data });
376 });
377 });
378 }
379
380 getServerLog() {
381 this.auth.allowed('s:console', (allowedErr, isAllowed) => {
382 if (allowedErr || !isAllowed) return;
383
384 this.auth.server().fs.readEnd(this.auth.server().service.object.log.location, (err, data) => {
385 if (err) {
386 return this.responses.generic500(err);
387 }
388 return this.res.send(data);
389 });
390 });
391 }
392
393 getServerFileStat() {
394 this.auth.allowed('s:files:read', (allowedErr, isAllowed) => {
395 if (allowedErr || !isAllowed) return;
396
397 this.auth.server().fs.stat(this.req.params[0], (err, data) => {
398 if (err) {
399 switch (err.code) {
400 case 'ENOENT':
401 return this.res.send(404);
402 default:
403 return this.responses.generic500(err);
404 }
405 }
406 return this.res.send(data);
407 });
408 });
409 }
410
411 postFileFolder() {
412 this.auth.allowed('s:files:create', (allowedErr, isAllowed) => {
413 if (allowedErr || !isAllowed) return;
414
415 this.auth.server().fs.mkdir(this.req.params.path, err => {
416 this.responses.generic204(err);
417 });
418 });
419 }
420
421 postFileCopy() {
422 this.auth.allowed('s:files:copy', (allowedErr, isAllowed) => {
423 if (allowedErr || !isAllowed) return;
424
425 this.auth.server().fs.copy(this.req.params.from, this.req.params.to, err => {
426 this.responses.generic204(err);
427 });
428 });
429 }
430
431 // prevent breaking API change for now.
432 deleteServerFile() {
433 this.auth.allowed('s:files:delete', (allowedErr, isAllowed) => {
434 if (allowedErr || !isAllowed) return;
435
436 this.auth.server().fs.rm(this.req.params[0], err => {
437 this.responses.generic204(err);
438 });
439 });
440 }
441
442 postFileDelete() {
443 this.auth.allowed('s:files:delete', (allowedErr, isAllowed) => {
444 if (allowedErr || !isAllowed) return;
445
446 this.auth.server().fs.rm(this.req.params.items, err => {
447 this.responses.generic204(err);
448 });
449 });
450 }
451
452 postFileMove() {
453 this.auth.allowed('s:files:move', (allowedErr, isAllowed) => {
454 if (allowedErr || !isAllowed) return;
455
456 this.auth.server().fs.move(this.req.params.from, this.req.params.to, err => {
457 this.responses.generic204(err);
458 });
459 });
460 }
461
462 postFileDecompress() {
463 this.auth.allowed('s:files:decompress', (allowedErr, isAllowed) => {
464 if (allowedErr || !isAllowed) return;
465
466 this.auth.server().fs.decompress(this.req.params.files, err => {
467 this.responses.generic204(err);
468 });
469 });
470 }
471
472 postFileCompress() {
473 this.auth.allowed('s:files:compress', (allowedErr, isAllowed) => {
474 if (allowedErr || !isAllowed) return;
475
476 this.auth.server().fs.compress(this.req.params.files, this.req.params.to, (err, filename) => {
477 if (err) {
478 return this.responses.generic500(err);
479 }
480 return this.res.send({
481 saved_as: filename,
482 });
483 });
484 });
485 }
486
487 postServerFile() {
488 this.auth.allowed('s:files:post', (allowedErr, isAllowed) => {
489 if (allowedErr || !isAllowed) return;
490
491 this.auth.server().fs.write(this.req.params.path, this.req.params.content, err => {
492 this.responses.generic204(err);
493 });
494 });
495 }
496
497 updateServerConfig() {
498 this.auth.allowed('g:server:patch', (allowedErr, isAllowed) => {
499 if (allowedErr || !isAllowed) return;
500
501 this.auth.server().modifyConfig(this.req.params, (this.req.method === 'PUT'), err => {
502 this.responses.generic204(err);
503 });
504 });
505 }
506
507 rebuildServer() {
508 this.auth.allowed('g:server:rebuild', (allowedErr, isAllowed) => {
509 if (allowedErr || !isAllowed) return;
510
511 this.auth.server().modifyConfig({ rebuild: true }, false, err => {
512 this.responses.generic204(err);
513 });
514 });
515 }
516
517 postServerSuspend() {
518 this.auth.allowed('g:server:suspend', (allowedErr, isAllowed) => {
519 if (allowedErr || !isAllowed) return;
520
521 this.auth.server().suspend(err => {
522 this.responses.generic204(err);
523 });
524 });
525 }
526
527 postServerUnsuspend() {
528 this.auth.allowed('g:server:unsuspend', (allowedErr, isAllowed) => {
529 if (allowedErr || !isAllowed) return;
530
531 this.auth.server().unsuspend(err => {
532 this.responses.generic204(err);
533 });
534 });
535 }
536
537 backupCreate() {
538 this.auth.allowed('s:backup', (allowedErr, isAllowed) => {
539 if (allowedErr || !isAllowed) return;
540
541 if ("name" in this.req.params === false)
542 return this.res.send({"success": "false", "error": "Missing name argument"});
543 if ("folder" in this.req.params === false)
544 return this.res.send({"success": "false", "error": "Missing folder argument"});
545
546 const fileName = this.req.params["name"];
547 const folder = this.req.params["folder"];
548
549 const auth = this.auth;
550 const uuid = this.auth.server().uuid;
551
552 Fs.access('/backup/' + uuid + '/' + fileName + '.zip', error => {
553 if (!error) {
554 this.res.send({"success": "false", "error": "Backup in this name already exists"});
555 } else {
556 this.auth.server().suspend(err => {
557 exec('mkdir /backup/' + uuid, function(err, stdout, stderr) {});
558
559 this.res.send({"success": "true"});
560
561 exec('cd /srv/daemon-data/' + uuid + folder + ' && zip -qr /backup/' + uuid + '/' + fileName + '.zip *', function(err, stdout, stderr) {
562 auth.server().unsuspend(err => {});
563
564 Request(`${Config.get('remote.base')}/api/remote/backup/completed`, {
565 method: 'POST',
566 json: {
567 server_uuid: uuid,
568 },
569 headers: {
570 'Accept': 'application/vnd.pterodactyl.v1+json',
571 'Authorization': `Bearer ${Config.get('keys.0')}`,
572 },
573 timeout: 5000,
574 }, (err, response, body) => {});
575 });
576 });
577 }
578 });
579 });
580}
581
582backupRestore() {
583 this.auth.allowed('s:backup', (allowedErr, isAllowed) => {
584 if (allowedErr || !isAllowed) return;
585
586 if ("name" in this.req.params === false)
587 return this.res.send({"success": "false", "error": "Missing name argument"});
588 if ("folder" in this.req.params === false)
589 return this.res.send({"success": "false", "error": "Missing folder argument"});
590
591 const fileName = this.req.params["name"];
592 const folder = this.req.params["folder"];
593
594 const uuid = this.auth.server().uuid;
595
596 const res = this.res;
597
598 Fs.access('/backup/' + uuid + '/' + fileName + '.zip', error => {
599 if (!error) {
600 this.auth.server().suspend(err => {
601 this.res.send({"success": "true"});
602
603 const auth = this.auth;
604
605 exec('unzip -qo /backup/' + uuid + '/' + fileName + '.zip -d /srv/daemon-data/' + uuid + folder, function (err, stdout, srderr) {
606 auth.server().unsuspend(err => {});
607
608 Request(`${Config.get('remote.base')}/api/remote/backup/completed`, {
609 method: 'POST',
610 json: {
611 server_uuid: uuid,
612 },
613 headers: {
614 'Accept': 'application/vnd.pterodactyl.v1+json',
615 'Authorization': `Bearer ${Config.get('keys.0')}`,
616 },
617 timeout: 5000,
618 }, (err, response, body) => {});
619 });
620 });
621 } else {
622 res.send({"success": "false", "error": "File not found: " + fileName + '.zip'});
623 }
624 });
625 });
626}
627
628backupDelete() {
629 this.auth.allowed('s:backup', (allowedErr, isAllowed) => {
630 if (allowedErr || !isAllowed) return;
631
632 if ("name" in this.req.params === false)
633 return this.res.send({"success": "false", "error": "Missing name argument"});
634
635 const fileName = this.req.params["name"];
636
637 const uuid = this.auth.server().uuid;
638
639 const res = this.res;
640
641 Fs.access('/backup/' + uuid + '/' + fileName + '.zip', error => {
642 if (!error) {
643 exec('rm -rf /backup/' + uuid + '/' + fileName + '.zip', function(err, stdout, stderr) {
644 if (err) {
645 res.send({"success": "false", "error": "Rm error"});
646 } else {
647 res.send({"success": "true"});
648 }
649 });
650 } else {
651 res.send({"success": "false", "error": "File not found: " + fileName + '.zip'});
652 }
653 });
654 });
655}
656
657backupDownload() {
658 Request(`${Config.get('remote.base')}/api/remote/backup/download-verify`, {
659 method: 'POST',
660 json: {
661 token: this.req.params.token,
662 },
663 headers: {
664 'Accept': 'application/vnd.pterodactyl.v1+json',
665 'Authorization': `Bearer ${Config.get('keys.0')}`,
666 },
667 timeout: 5000,
668 }, (err, response, body) => {
669 if (err) {
670 return this.res.send(500, { "error": "An error occured while attempting to perform this request." });
671 }
672
673 if (response.statusCode === 200) {
674 try {
675 const json = _.isString(body) ? JSON.parse(body) : body;
676 if (!_.isUndefined(json) && json.path && json.name) {
677 const Server = this.auth.allServers();
678 if (_.isUndefined(Server[json.server])) {
679 return this.res.send(404, { 'error': 'No server found for the specified resource.' });
680 }
681
682 const uuid = json.server;
683 const fileName = json.path;
684 const origName = json.name;
685
686 const Mimetype = Mime.getType('/backup/' + uuid + '/' + fileName + '.zip');
687 const Stat = Fs.statSync('/backup/' + uuid + '/' + fileName + '.zip');
688 if (!Stat.isFile()) {
689 return this.res.send({"success": "false", "error": "Could not locate the requested file."});
690 }
691
692 this.res.writeHead(200, {
693 "Content-Disposition": "attachment; filename=" + origName + ".zip",
694 'Content-Type': Mimetype,
695 'Content-Length': Stat.size
696 });
697
698 const readStream = Fs.createReadStream('/backup/' + uuid + '/' + fileName + '.zip');
699 readStream.pipe(this.res);
700 } else {
701 return this.res.send(424, { 'error': 'The upstream response did not include a valid download path.' });
702 }
703 } catch (ex) {
704 return this.res.send(500, { 'error': 'An unexpected error occured while attempting to process this request.' + ex });
705 }
706 } else {
707 this.res.redirect(this.req.header('Referer') || Config.get('remote.base'), _.constant(''));
708 }
709 });
710}
711
712downloadPlugin() {
713 this.auth.allowed('s:plugins', (allowedErr, isAllowed) => {
714 if (allowedErr || !isAllowed) return;
715
716 if ("id" in this.req.params === false)
717 return this.res.send({"success": "false", "error": "Missing id argument"});
718
719 const id = this.req.params["id"];
720
721 const uuid = this.auth.server().uuid;
722 const res = this.res;
723
724 http.get('http://api.spiget.org/v2/resources/' + id + '/download', function(response) {
725 const contentDisposition = response.headers['content-disposition'];
726 const match = contentDisposition && contentDisposition.match(/(filename=|filename\*='')(.*)$/);
727 let filename = match && match[2] || 'default-plugin-name';
728 filename = filename.replace('"', '');
729 filename = filename.replace('"', '');
730 filename = filename.split('#')[0] + '.jar';
731
732 const file = Fs.createWriteStream("/srv/daemon-data/" + uuid + "/plugins/" + filename);
733
734 response.pipe(file);
735
736 exec('chown pterodactyl:pterodactyl /srv/daemon-data/' + uuid + '/plugins/' + filename, function(err, stdout, stderr) {
737 res.send({"success": "true", "name": filename});
738 });
739 });
740 });
741}
742
743deletePlugin() {
744 this.auth.allowed('s:plugins', (allowedErr, isAllowed) => {
745 if (allowedErr || !isAllowed) return;
746
747 if ("name" in this.req.params === false)
748 return this.res.send({"success": "false", "error": "Missing name argument"});
749
750 const name = this.req.params["name"];
751
752 const uuid = this.auth.server().uuid;
753 const res = this.res;
754
755 exec('rm -rf /srv/daemon-data/' + uuid + '/plugins/' + name, function(err, stdout, stderr) {
756 res.send({"success": "true"});
757 });
758 });
759}
760
761 downloadServerFile() {
762 Request(`${Config.get('remote.base')}/api/remote/download-file`, {
763 method: 'POST',
764 json: {
765 token: this.req.params.token,
766 },
767 headers: {
768 'Accept': 'application/vnd.pterodactyl.v1+json',
769 'Authorization': `Bearer ${Config.get('keys.0')}`,
770 },
771 timeout: 5000,
772 }, (err, response, body) => {
773 if (err) {
774 Log.warn(err, 'Download action failed due to an error with the request.');
775 return this.res.send(500, { 'error': 'An error occured while attempting to perform this request.' });
776 }
777
778 if (response.statusCode === 200) {
779 try {
780 const json = _.isString(body) ? JSON.parse(body) : body;
781 if (!_.isUndefined(json) && json.path) {
782 const Server = this.auth.allServers();
783 // Does the server even exist?
784 if (_.isUndefined(Server[json.server])) {
785 return this.res.send(404, { 'error': 'No server found for the specified resource.' });
786 }
787
788 // Get necessary information for the download.
789 const Filename = Path.basename(json.path);
790 const Mimetype = Mime.getType(json.path);
791 const File = Server[json.server].path(json.path);
792 const Stat = Fs.statSync(File);
793 if (!Stat.isFile()) {
794 return this.res.send(404, { 'error': 'Could not locate the requested file.' });
795 }
796
797 this.res.writeHead(200, {
798 'Content-Type': Mimetype,
799 'Content-Length': Stat.size,
800 'Content-Disposition': Util.format('attachment; filename=%s', Filename),
801 });
802 const Filestream = Fs.createReadStream(File);
803 Filestream.pipe(this.res);
804 } else {
805 return this.res.send(424, { 'error': 'The upstream response did not include a valid download path.' });
806 }
807 } catch (ex) {
808 Log.error(ex);
809 return this.res.send(500, { 'error': 'An unexpected error occured while attempting to process this request.' });
810 }
811 } else {
812 if (response.statusCode >= 500) {
813 Log.warn({ res_code: response.statusCode, res_body: body }, 'An error occured while attempting to retrieve file download information for an upstream provider.');
814 }
815
816 this.res.redirect(this.req.header('Referer') || Config.get('remote.base'), _.constant(''));
817 }
818 });
819 }
820}
821
822module.exports = RouteController;