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