├── .babelrc ├── .docker ├── entrypoint.sh └── supervisord.conf ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── Vagrantfile ├── config ├── .gitkeep ├── credentials │ └── .gitkeep └── servers │ └── .gitkeep ├── docker-compose.yml ├── logs └── .gitkeep ├── package-lock.json ├── package.json ├── packs └── .gitkeep ├── scripts ├── configure.js ├── diagnostics.js └── migrate.js └── src ├── controllers ├── builder.js ├── delete.js ├── docker.js ├── fs.js ├── network.js ├── option.js ├── pack.js ├── routes.js ├── server.js └── service.js ├── errors ├── file_parse_error.js ├── index.js └── no_egg_config.js ├── helpers ├── config.js ├── fileparser.js ├── image.js ├── initialize.js ├── logger.js ├── responses.js ├── sftpqueue.js ├── status.js └── timezone.js ├── http ├── restify.js ├── routes.js ├── sftp.js ├── socket.js ├── stats.js └── upload.js ├── index.js ├── middleware └── authorizable.js └── services ├── configs └── .githold └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-node4"] 3 | } 4 | -------------------------------------------------------------------------------- /.docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | ## Ensure we are in /srv/daemon 3 | 4 | if [ $(cat /srv/daemon/config/core.json | jq -r '.sftp.enabled') == "null" ]; then 5 | echo -e "Updating config to enable sftp-server." 6 | cat /srv/daemon/config/core.json | jq '.sftp.enabled |= false' > /tmp/core 7 | cat /tmp/core > /srv/daemon/config/core.json 8 | elif [ $(cat /srv/daemon/config/core.json | jq -r '.sftp.enabled') == "false" ]; then 9 | echo -e "Config already set up for golang sftp server" 10 | else 11 | echo -e "You may have purposly set the sftp to true and that will fail." 12 | fi 13 | 14 | exec "$@" -------------------------------------------------------------------------------- /.docker/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/tmp/supervisor.sock ; path to your socket file 3 | 4 | [supervisord] 5 | logfile=/var/log/supervisord/supervisord.log ; supervisord log file 6 | logfile_maxbytes=50MB ; maximum size of logfile before rotation 7 | logfile_backups=2 ; number of backed up logfiles 8 | loglevel=error ; info, debug, warn, trace 9 | pidfile=/var/run/supervisord.pid ; pidfile location 10 | nodaemon=false ; run supervisord as a daemon 11 | minfds=1024 ; number of startup file descriptors 12 | minprocs=200 ; number of process descriptors 13 | user=root ; default user 14 | childlogdir=/var/log/supervisord/ ; where child log files will live 15 | 16 | [rpcinterface:supervisor] 17 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 18 | 19 | [supervisorctl] 20 | serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket 21 | 22 | [program:daemon] 23 | command=/usr/local/bin/node /srv/daemon/src/index.js 24 | autostart=true 25 | autorestart=true 26 | stdout_logfile=/dev/fd/1 27 | stdout_logfile_maxbytes=0 28 | 29 | [program:sftp-server] 30 | command=/srv/daemon/sftp-server 31 | autostart=true 32 | autorestart=true 33 | stdout_logfile=/dev/fd/1 34 | stdout_logfile_maxbytes=0 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore JSON Files 2 | **/*.json 3 | 4 | src/services/**/*.js 5 | node_modules 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "lodash" 4 | ], 5 | "extends": [ 6 | "airbnb", 7 | "plugin:lodash/recommended" 8 | ], 9 | "rules": { 10 | "indent": [2, 4], 11 | "func-names": 0, 12 | "strict": 0, 13 | "quote-props": 0, 14 | "consistent-return": 0, 15 | "max-len": 0, 16 | "lodash/prefer-map": 0, 17 | "lodash/import-scope": 0, 18 | "no-multi-assign": 2, 19 | "arrow-parens": [2, "as-needed"], 20 | "class-methods-use-this": 0, 21 | "prefer-destructuring": 0, 22 | "no-bitwise": 0, 23 | "comma-dangle": ["error", { 24 | "arrays": "always-multiline", 25 | "objects": "always-multiline", 26 | "imports": "always-multiline", 27 | "exports": "always-multiline", 28 | "functions": "never" 29 | }] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | node_modules 4 | nbproject 5 | config/**/* 6 | packs/** 7 | .vagrant/ 8 | .vscode/** 9 | .DS_Store 10 | .idea/** 11 | src/services/configs/*.json 12 | !src/services/index.js 13 | !config/.githold 14 | !config/servers/.githold 15 | !logs/.githold 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | env: 6 | - CXX=g++-4.8 7 | cache: 8 | yarn: true 9 | directories: 10 | - $HOME/.npm 11 | addons: 12 | apt: 13 | sources: 14 | - ubuntu-toolchain-r-test 15 | packages: 16 | - g++-4.8 17 | notifications: 18 | email: false 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | LABEL author="Michael Parker" maintainer="parker@pterodactyl.io" 4 | 5 | COPY . /srv/daemon 6 | 7 | WORKDIR /srv/daemon 8 | 9 | RUN apk add --no-cache openssl make gcc g++ python linux-headers paxctl gnupg tar zip unzip curl coreutils zlib supervisor jq \ 10 | && npm install --production \ 11 | && addgroup -S pterodactyl && adduser -S -D -H -G pterodactyl -s /bin/false pterodactyl \ 12 | && apk del --no-cache make gcc g++ python linux-headers paxctl gnupg \ 13 | && curl -sSL https://github.com/pterodactyl/sftp-server/releases/download/v1.0.4/sftp-server > /srv/daemon/sftp-server \ 14 | && mkdir -p /var/log/supervisord /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 \ 15 | && chmod +x /srv/daemon/sftp-server \ 16 | && chmod +x /srv/daemon/.docker/entrypoint.sh \ 17 | && cp /srv/daemon/.docker/supervisord.conf /etc/supervisord.conf 18 | 19 | EXPOSE 8080 20 | 21 | ENTRYPOINT [ "/bin/ash", "/srv/daemon/.docker/entrypoint.sh" ] 22 | 23 | CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Pterodactyl - Daemon 2 | Copyright (c) 2015 - 2020 Dane Everitt . 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pterodactyl Daemon [DEPRECATED] 2 | This repository contains the deprecated control software for Pterodactyl Panel. You should upgrade to [Wings](https://github.com/pterodactyl/wings) which is the new software that provides a much more stable and secure expierence. 3 | 4 | This code remains available for historical reasons. **This repository will be archived and made read-only on March 1st 2021.** 5 | 6 | ### Contributing 7 | **Pull requests will not be accepted to this repository for any reason other than _critical_ security fixes.** 8 | 9 | ### License 10 | ``` 11 | Pterodactyl - Daemon 12 | Copyright (c) 2015 - 2020 Dane Everitt . 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | ``` 32 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "bento/ubuntu-16.04" 3 | 4 | config.vm.synced_folder "./", "/srv/daemon", 5 | owner: "root", group: "root" 6 | 7 | config.vm.provision :shell, path: ".dev/vagrant/provision.sh" 8 | 9 | config.vm.network :private_network, ip: "192.168.50.3" 10 | config.vm.network :forwarded_port, guest: 8080, host: 58080 11 | config.vm.network :forwarded_port, guest: 2022, host: 52022 12 | end 13 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pterodactyl/daemon/ca642f399528495bfde22760898d6aad297d3e9e/config/.gitkeep -------------------------------------------------------------------------------- /config/credentials/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pterodactyl/daemon/ca642f399528495bfde22760898d6aad297d3e9e/config/credentials/.gitkeep -------------------------------------------------------------------------------- /config/servers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pterodactyl/daemon/ca642f399528495bfde22760898d6aad297d3e9e/config/servers/.gitkeep -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | daemon: 4 | build: . 5 | restart: always 6 | hostname: daemon 7 | ports: 8 | - "8080:8080" 9 | - "2022:2022" 10 | tty: true 11 | volumes: 12 | - "/var/run/docker.sock:/var/run/docker.sock" 13 | - "/var/lib/docker/containers:/var/lib/docker/containers" 14 | - "/srv/daemon/config/:/srv/daemon/config/" 15 | - "/srv/daemon-data/:/srv/daemon-data/" 16 | - "/tmp/pterodactyl/:/tmp/pterodactyl" 17 | ## Required for ssl if you user let's encrypt. uncomment to use. 18 | ## - "/etc/letsencrypt/:/etc/letsencrypt/" 19 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pteronode", 3 | "version": "0.0.0-canary", 4 | "description": "Server management wrapper built for Pterodactyl Panel.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "node src/index.js | node_modules/bunyan/bin/bunyan -o short", 11 | "test": "./node_modules/eslint/bin/eslint.js --quiet --config .eslintrc src scripts", 12 | "configure": "node scripts/configure.js", 13 | "diagnostics": "node scripts/diagnostics.js", 14 | "migrate": "node scripts/migrate.js" 15 | }, 16 | "repository": "git+https://github.com/pterodactyl/daemon.git", 17 | "keywords": [ 18 | "pterodactyl", 19 | "daemon", 20 | "wings" 21 | ], 22 | "author": "Dane Everitt ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/pterodactyl/daemon/issues" 26 | }, 27 | "engines": { 28 | "node": ">=8.0.0" 29 | }, 30 | "os": [ 31 | "linux" 32 | ], 33 | "homepage": "https://github.com/pterodactyl/daemon", 34 | "devDependencies": { 35 | "eslint": "4.16.0", 36 | "eslint-config-airbnb": "16.1.0", 37 | "eslint-plugin-import": "2.8.0", 38 | "eslint-plugin-jsx-a11y": "6.0.3", 39 | "eslint-plugin-lodash": "2.5.0", 40 | "eslint-plugin-react": "7.5.1" 41 | }, 42 | "dependencies": { 43 | "ansi-escape-sequences": "^5.1.2", 44 | "async": "2.6.1", 45 | "bunyan": "^1.8", 46 | "carrier": "^0.3", 47 | "chokidar": "^3.3.1", 48 | "compare-versions": "^3.2", 49 | "create-output-stream": "^0.0.1", 50 | "dockerode": "2.5.8", 51 | "extendify": "^1.0", 52 | "fs-extra": "^8.1.0", 53 | "getos": "^3.1", 54 | "ini": "^1.3", 55 | "inquirer": "^5.0", 56 | "ip-cidr": "^2.0.7", 57 | "isstream": "^0.1", 58 | "keypair": "^1.0", 59 | "klaw": "^2.1", 60 | "lodash": "^4.17.15", 61 | "memory-cache": "^0.2", 62 | "mime": "^2.4.4", 63 | "mmmagic": "^0.5.3", 64 | "moment": "^2.24.0", 65 | "node-yaml": "^3.1", 66 | "properties-parser": "^0.3", 67 | "randomstring": "^1.1", 68 | "request": "^2.87.0", 69 | "restify": "^4.3.4", 70 | "rfr": "^1.2", 71 | "socket.io": "^2.0", 72 | "socketio-file-upload": "^0.6", 73 | "ssh2": "^0.6.1", 74 | "ssh2-streams": "^0.2.1", 75 | "tail": "^1.3.0", 76 | "xml2js": "^0.4", 77 | "yargs": "^10.1" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pterodactyl/daemon/ca642f399528495bfde22760898d6aad297d3e9e/packs/.gitkeep -------------------------------------------------------------------------------- /scripts/configure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | 26 | /* eslint no-console: 0 */ 27 | const _ = require('lodash'); 28 | const Inquirer = require('inquirer'); 29 | const Request = require('request'); 30 | const Fs = require('fs-extra'); 31 | const Path = require('path'); 32 | 33 | const CONFIG_PATH = Path.resolve('config/core.json'); 34 | const CONFIG_EXISTS = Fs.existsSync(CONFIG_PATH); 35 | 36 | const regex = { 37 | fqdn: new RegExp(/^https?:\/\/(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,64}\/?$/), 38 | ipv4: new RegExp(/^(?:https?:\/\/)?(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.|\/?$)){4}/), 39 | }; 40 | 41 | const params = { 42 | panelurl: { 43 | name: 'Panel url', 44 | cmdoption: 'p', 45 | validate: value => { 46 | if (value === '') { 47 | return 'The panel url is required'; 48 | } 49 | if (!value.match(regex.fqdn) && !value.match(regex.ipv4)) { 50 | return 'Please provide either a URL (with protocol) or an IPv4 address.'; 51 | } 52 | return true; 53 | }, 54 | }, 55 | token: { 56 | name: 'Configuration token', 57 | cmdoption: 't', 58 | validate: value => { 59 | if (value === '') { 60 | return 'A configuration token is required.'; 61 | } 62 | if (value.length === 32) { 63 | return true; 64 | } 65 | return 'The token provided is invalid. It must be 32 characters long.'; 66 | }, 67 | }, 68 | overwrite: { 69 | name: 'Overwrite', 70 | cmdoption: 'c', 71 | }, 72 | }; 73 | 74 | /** 75 | * yargs compatible method for validating the arguments 76 | */ 77 | function checkParams(arg) { 78 | _.forEach(params, param => { 79 | if (_.has(arg, param.cmdoption)) { 80 | const valResult = param.validate(arg[param.cmdoption]); 81 | if (valResult !== true) { 82 | throw new Error(`${param.name}: ${valResult}`); 83 | } 84 | } 85 | }); 86 | return true; 87 | } 88 | 89 | // Parse provided arguments with yargs for insanly easier usage 90 | const argv = require('yargs') 91 | .usage('npm run configure -- ') 92 | .describe('p', 'The panel url to pull the configuration from') 93 | .alias('p', 'panel-url') 94 | .describe('t', 'The configuration token') 95 | .alias('t', 'token') 96 | .boolean('o') 97 | .describe('o', 'Overwrite existing configuration file') 98 | .alias('o', 'overwrite') 99 | .alias('h', 'help') 100 | .check(checkParams) 101 | .help('h') 102 | .fail((msg, err, yargs) => { 103 | console.error(err.message); 104 | console.log(yargs.help()); 105 | process.exit(1); 106 | }) 107 | .argv; 108 | 109 | // Put the provided values into the overly complicated params object 110 | _.forEach(params, (param, key) => { 111 | if (_.has(argv, param.cmdoption)) { 112 | params[key].value = argv[param.cmdoption]; 113 | } 114 | }); 115 | 116 | // Check if the configuration file exists already 117 | if (CONFIG_EXISTS) console.log('Configuration file (core.json) exists.'); 118 | 119 | // Interactive questioning of missing parameters 120 | Inquirer.prompt([ 121 | { 122 | name: 'panelurl', 123 | type: 'string', 124 | message: 'Url of the panel to add the node to:', 125 | when: () => _.isUndefined(params.panelurl.value), 126 | validate: params.panelurl.validate, 127 | }, { 128 | name: 'token', 129 | type: 'string', 130 | message: 'A configuration token to use:', 131 | when: () => _.isUndefined(params.token.value), 132 | validate: params.token.validate, 133 | }, { 134 | // Will only be asked if file exists and no overwrite flag is set 135 | name: 'overwrite', 136 | type: 'confirm', 137 | default: false, 138 | message: 'Overwrite existing configuration?', 139 | when: () => !params.overwrite.value && CONFIG_EXISTS, 140 | }, 141 | ]).then(answers => { 142 | // Overwrite the values in the overly complicated params object 143 | _.forEach(answers, (answer, key) => { 144 | params[key].value = answer; 145 | }); 146 | 147 | // If file exists and no overwrite wanted error and exit. 148 | if (!params.overwrite.value && CONFIG_EXISTS) { 149 | console.error('Configuration file already exists, no overwrite requested. Aborting.'); 150 | process.exit(); 151 | } 152 | 153 | // Fetch configuration from the panel 154 | console.log('Fetching configuration from panel.'); 155 | Request.get(`${params.panelurl.value}/daemon/configure/${params.token.value}`, (error, response, body) => { 156 | if (!error) { 157 | // response should always be JSON 158 | const jsonBody = JSON.parse(body); 159 | 160 | if (response.statusCode === 200) { 161 | console.log('Writing configuration to file.'); 162 | 163 | Fs.writeFile(CONFIG_PATH, JSON.stringify(jsonBody, null, 4), err => { 164 | if (err) { 165 | console.error('Failed to write configuration file.'); 166 | } else { 167 | console.log('Configuration file written successfully.'); 168 | } 169 | }); 170 | } else if (response.statusCode === 403) { 171 | if (_.get(jsonBody, 'error') === 'token_invalid') { 172 | console.error('The token you used is invalid.'); 173 | } else if (_.get(jsonBody, 'error') === 'token_expired') { 174 | console.error('The token provided is expired.'); 175 | } else { 176 | console.error('An unknown error occured!', body); 177 | } 178 | } else { 179 | console.error('Sorry. Something went wrong fetching the configuration.'); 180 | } 181 | } else { 182 | console.error('Sorry. Something went wrong fetching the configuration.'); 183 | } 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /scripts/diagnostics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | 26 | const Async = require('async'); 27 | const Inquirer = require('inquirer'); 28 | const Request = require('request'); 29 | const Process = require('child_process'); 30 | const Fs = require('fs-extra'); 31 | const _ = require('lodash'); 32 | const Path = require('path'); 33 | const Package = require('../package.json'); 34 | 35 | function postToHastebin(text) { 36 | return new Promise((resolve, reject) => { 37 | Request.post({ 38 | uri: 'https://hastebin.com/documents', 39 | body: text, 40 | }, (error, response, body) => { 41 | if (error || response.statusCode !== 200) { 42 | reject(error); 43 | } else { 44 | resolve(`https://hastebin.com/${JSON.parse(body).key.toString()}`); 45 | } 46 | }); 47 | }); 48 | } 49 | function postToPtero(text) { 50 | return new Promise((resolve, reject) => { 51 | Request.post({ 52 | uri: 'https://ptero.co/documents', 53 | body: text, 54 | }, (error, response, body) => { 55 | if (error || response.statusCode !== 200) { 56 | reject(error); 57 | } else { 58 | resolve(`https://ptero.co/${JSON.parse(body).key.toString()}`); 59 | } 60 | }); 61 | }); 62 | } 63 | 64 | Inquirer.prompt([ 65 | { 66 | name: 'endpoints', 67 | type: 'confirm', 68 | message: 'Do you want to include Endpoints (i.e. the FQDN/IP of your Panel)', 69 | default: true, 70 | }, { 71 | name: 'logs', 72 | type: 'confirm', 73 | message: 'Do you want to include the latest logs?', 74 | default: true, 75 | }, { 76 | name: 'hastebin', 77 | type: 'confirm', 78 | message: 'Do you directly want to upload the diagnostics to hastebin.com / ptero.co?', 79 | default: true, 80 | }, 81 | ]).then(answers => { 82 | Async.auto({ 83 | config: callback => { 84 | Fs.access('./config/core.json', (Fs.constants || Fs).R_OK | (Fs.constants || Fs).W_OK, err => { // eslint-disable-line 85 | if (err) return callback(null, { error: true }); 86 | return callback(null, Fs.readJsonSync('./config/core.json')); 87 | }); 88 | }, 89 | docker_version: callback => { 90 | Process.exec('docker --version', (err, stdout) => { 91 | callback(err, stdout); 92 | }); 93 | }, 94 | docker_info: callback => { 95 | Process.exec('docker info', (err, stdout) => { 96 | callback(err, stdout); 97 | }); 98 | }, 99 | docker_containers: callback => { 100 | Process.exec('docker ps --format \'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\' -a', (err, stdout) => { 101 | callback(err, stdout); 102 | }); 103 | }, 104 | bunyan_logs: callback => { 105 | if (answers.logs) { 106 | const MainLog = Fs.existsSync(Path.resolve('logs/wings.log')); 107 | const SecondLog = Fs.existsSync(Path.resolve('logs/wings.log.0')); 108 | 109 | let logFile; 110 | if (MainLog) { 111 | logFile = 'logs/wings.log'; 112 | } else if (SecondLog) { 113 | logFile = 'logs/wings.log.0'; 114 | } else { 115 | return callback(null, '[no logs found]'); 116 | } 117 | 118 | Process.exec(`tail -n 200 ${logFile} | ./node_modules/bunyan/bin/bunyan -o short`, { maxBuffer: 100 * 1024 }, (err, stdout) => { 119 | callback(err, stdout); 120 | }); 121 | } else { 122 | return callback(null, '[not provided]'); 123 | } 124 | }, 125 | }, (err, results) => { 126 | if (err) return console.error(err); // eslint-disable-line 127 | 128 | const remoteHost = (answers.endpoints) ? _.get(results.config, 'remote.base', null) : '[redacted]'; 129 | const outputFormat = ` 130 | ==[ Pterodactyl Daemon: Diagnostics Report ]== 131 | --| Software Versions 132 | - Daemon: ${Package.version} 133 | - Node.js: ${process.version} 134 | - Docker: ${results.docker_version} 135 | 136 | --| Daemon Configuration 137 | - SSL Enabled: ${_.get(results.config, 'web.ssl.enabled', null)} 138 | - Port: ${_.get(results.config, 'web.listen', null)} 139 | - Upload Size: ${_.get(results.config, 'uploads.size_limit', null)} 140 | - Remote Host: ${remoteHost} 141 | 142 | --| Docker Information 143 | ${results.docker_info} 144 | 145 | --| Docker Containers 146 | ${results.docker_containers} 147 | 148 | --| Latest Logs 149 | ${results.bunyan_logs} 150 | ==[ END DIAGNOSTICS ]==`; 151 | 152 | if (answers.hastebin) { 153 | postToHastebin(outputFormat) 154 | .then(url => { 155 | console.log('Your diagnostics report is available at:', url); // eslint-disable-line 156 | }) 157 | .catch(error => { 158 | console.error('An error occured while uploading to hastebin.com. Attempting to upload to ptero.co', error); // eslint-disable-line 159 | postToPtero(outputFormat) 160 | .then(url => { 161 | console.log('Your diagnostics report is available at:', url); // eslint-disable-line 162 | }) 163 | .catch(error => { // eslint-disable-line 164 | console.error('An error occured while uploading to hastebin.com & ptero.co', error); // eslint-disable-line 165 | }); 166 | }); 167 | } else { 168 | console.log(outputFormat); // eslint-disable-line 169 | } 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /scripts/migrate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | 26 | const _ = require('lodash'); 27 | const rfr = require('rfr'); 28 | const Async = require('async'); 29 | const Inquirer = require('inquirer'); 30 | const Klaw = require('klaw'); 31 | const Fs = require('fs-extra'); 32 | const Path = require('path'); 33 | const Process = require('child_process'); 34 | const Util = require('util'); 35 | 36 | const ConfigHelper = rfr('src/helpers/config.js'); 37 | const Config = new ConfigHelper(); 38 | 39 | Inquirer.prompt([ 40 | { 41 | name: 'perform', 42 | type: 'confirm', 43 | message: 'Are you sure you wish to migrate server data locations?', 44 | default: false, 45 | }, 46 | { 47 | name: 'stopped', 48 | type: 'confirm', 49 | message: 'Have you stopped *ALL* running server instances and the Daemon?', 50 | default: false, 51 | }, 52 | { 53 | name: 'backup', 54 | type: 'confirm', 55 | message: 'Have you backed up your server data?', 56 | default: false, 57 | }, 58 | { 59 | name: 'docker', 60 | type: 'confirm', 61 | message: 'Are you aware that you will need to run `docker system prune` after completing this migration?', 62 | default: false, 63 | }, 64 | ]).then(answers => { 65 | if (!answers.perform || !answers.stopped || !answers.backup || !answers.docker) { 66 | process.exit(); 67 | } 68 | 69 | console.log('Beginning data migration, do not exit this script or power off the server.'); 70 | console.log('This may take some time to run depending on the size of server folders.'); 71 | 72 | this.folders = []; 73 | Async.series([ 74 | callback => { 75 | Klaw('./config/servers/').on('data', data => { 76 | this.folders.push(data.path); 77 | }).on('end', callback); 78 | }, 79 | callback => { 80 | Async.eachOfLimit(this.folders, 2, (file, k, ecall) => { 81 | if (Path.extname(file) === '.json') { 82 | Async.auto({ 83 | json: acall => { 84 | Fs.readJson(file, acall); 85 | }, 86 | paths: ['json', (r, acall) => { 87 | const CurrentPath = Path.join(Config.get('sftp.path', '/srv/daemon-data'), _.get(r.json, 'user', '_SHOULD_SKIP'), '/data'); 88 | const NewPath = Path.join(Config.get('sftp.path', '/srv/daemon-data'), r.json.uuid); 89 | 90 | return acall(null, { 91 | currentPath: CurrentPath, 92 | newPath: NewPath, 93 | }); 94 | }], 95 | exists: ['paths', (r, acall) => { 96 | Fs.access(r.paths.currentPath, Fs.constants.F_OK | Fs.constants.R_OK, err => { 97 | acall(null, !err); 98 | }); 99 | }], 100 | directory: ['exists', (r, acall) => { 101 | if (r.exists) { 102 | Fs.ensureDir(r.paths.newPath, acall); 103 | } else { 104 | return acall(); 105 | } 106 | }], 107 | move: ['directory', (r, acall) => { 108 | if (r.exists) { 109 | console.log(`Moving files from ${r.paths.currentPath} to ${r.paths.newPath}`); 110 | Process.exec(`mv ${r.paths.currentPath}/* ${r.paths.newPath}/.`, {}, acall); 111 | } else { 112 | console.log(`Skipping move for ${r.paths.currentPath} as folder does not exist.`); 113 | return acall(); 114 | } 115 | }], 116 | cleanup: ['move', (r, acall) => { 117 | if (r.exists) { 118 | console.log(`Removing old data directory: ${Path.join(r.paths.currentPath, '../')}`); 119 | Fs.remove(Path.join(r.paths.currentPath, '../'), acall); 120 | } else { 121 | return acall(); 122 | } 123 | }], 124 | }, ecall); 125 | } else { 126 | return ecall(); 127 | } 128 | }, callback); 129 | }, 130 | callback => { 131 | console.log('Setting correct ownership of files.'); 132 | const Exec = Process.spawn('chown', ['-R', Util.format('%d:%d', Config.get('docker.container.user', 1000), Config.get('docker.container.user', 1000)), Config.get('sftp.path', '/srv/daemon-data')]); 133 | Exec.on('error', callback); 134 | Exec.on('exit', () => { callback(); }); 135 | }, 136 | ], err => { 137 | if (err) return console.error(err); 138 | 139 | console.log('Completed move of all server files to new data structure.'); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/controllers/builder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Async = require('async'); 27 | const _ = require('lodash'); 28 | const Fs = require('fs-extra'); 29 | const Path = require('path'); 30 | 31 | const Log = rfr('src/helpers/logger.js'); 32 | const ConfigHelper = rfr('src/helpers/config.js'); 33 | const InitializeHelper = rfr('src/helpers/initialize.js').Initialize; 34 | const DeleteController = rfr('src/controllers/delete.js'); 35 | 36 | const Initialize = new InitializeHelper(); 37 | const Config = new ConfigHelper(); 38 | 39 | class Builder { 40 | constructor(json) { 41 | if (!json || !_.isObject(json) || json === null || !_.keys(json).length) { 42 | throw new Error('Invalid JSON was passed to Builder.'); 43 | } 44 | this.json = json; 45 | this.log = Log.child({ server: this.json.uuid }); 46 | } 47 | 48 | init(next) { 49 | Async.auto({ 50 | create_folder: callback => { 51 | Fs.ensureDir(Path.join(Config.get('sftp.path', '/srv/daemon-data'), this.json.uuid), callback); 52 | }, 53 | verify_ip: callback => { 54 | this.log.debug('Updating passed JSON to route correct interfaces.'); 55 | // Update 127.0.0.1 to point to the docker0 interface. 56 | if (this.json.build.default.ip === '127.0.0.1') { 57 | this.json.build.default.ip = Config.get('docker.network.ispn', false) ? '' : Config.get('docker.interface'); 58 | } 59 | Async.forEachOf(this.json.build.ports, (ports, ip, asyncCallback) => { 60 | if (ip === '127.0.0.1') { 61 | if (!Config.get('docker.network.ispn', false)) { 62 | this.json.build.ports[Config.get('docker.interface')] = ports; 63 | } 64 | delete this.json.build.ports[ip]; 65 | return asyncCallback(); 66 | } 67 | return asyncCallback(); 68 | }, callback); 69 | }, 70 | initialize: ['create_folder', 'verify_ip', (results, callback) => { 71 | Initialize.setup(this.json, callback); 72 | }], 73 | block_boot: ['initialize', (results, callback) => { 74 | results.initialize.blockStartup(true, callback); 75 | }], 76 | install_pack: ['block_boot', (results, callback) => { 77 | results.initialize.pack.install(callback); 78 | }], 79 | run_scripts: ['install_pack', (results, callback) => { 80 | if (_.get(this.json, 'service.skip_scripts', false)) { 81 | this.log.info('Skipping egg script run due to server configuration file.'); 82 | return callback(); 83 | } 84 | results.initialize.option.install(callback); 85 | }], 86 | unblock_boot: ['run_scripts', (results, callback) => { 87 | results.initialize.blockStartup(false, callback); 88 | }], 89 | }, err => { 90 | next(err, this.json); 91 | 92 | // Delete the server if there was an error causing this builder to abort. 93 | if (err) { 94 | const Delete = new DeleteController(this.json); 95 | Delete.delete(deleteError => { 96 | if (deleteError) Log.error(deleteError); 97 | }); 98 | } 99 | }); 100 | } 101 | } 102 | 103 | module.exports = Builder; 104 | -------------------------------------------------------------------------------- /src/controllers/delete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Async = require('async'); 27 | const Dockerode = require('dockerode'); 28 | const Fs = require('fs-extra'); 29 | const Path = require('path'); 30 | const _ = require('lodash'); 31 | const isStream = require('isstream'); 32 | 33 | const ConfigHelper = rfr('src/helpers/config.js'); 34 | const Log = rfr('src/helpers/logger.js'); 35 | const Status = rfr('src/helpers/status.js'); 36 | 37 | const Config = new ConfigHelper(); 38 | const DockerController = new Dockerode({ 39 | socketPath: Config.get('docker.socket', '/var/run/docker.sock'), 40 | }); 41 | 42 | class Delete { 43 | constructor(json) { 44 | this.json = json; 45 | this.log = Log.child({ server: this.json.uuid }); 46 | } 47 | 48 | delete(next) { 49 | Async.auto({ 50 | // Clear the 'Servers' object of the specific server 51 | clear_object: callback => { 52 | this.log.debug('Clearing servers object...'); 53 | const Servers = rfr('src/helpers/initialize.js').Servers; 54 | 55 | // Prevent crash detection 56 | if (!_.isUndefined(Servers[this.json.uuid]) && _.isFunction(Servers[this.json.uuid].setStatus)) { 57 | const server = Servers[this.json.uuid]; 58 | 59 | clearInterval(server.intervals.diskUse); 60 | 61 | if (!_.isNil(server.docker)) { 62 | if (isStream(server.docker.stream)) { 63 | server.docker.stream.end(); 64 | } 65 | 66 | if (!_.isNil(server.docker.logStream)) { 67 | server.docker.logStream.unwatch(); 68 | } 69 | } 70 | 71 | Servers[this.json.uuid].setStatus(Status.OFF); 72 | } 73 | 74 | delete Servers[this.json.uuid]; 75 | return callback(); 76 | }, 77 | // Delete the container (kills if running) 78 | delete_container: ['clear_object', (r, callback) => { 79 | this.log.debug('Attempting to remove container...'); 80 | 81 | const container = DockerController.getContainer(this.json.uuid); 82 | container.inspect(err => { 83 | if (!err) { 84 | container.remove({ v: true, force: true }, rErr => { 85 | if (!rErr) this.log.debug('Removed docker container from system.'); 86 | callback(rErr); 87 | }); 88 | } else if (err && _.startsWith(err.reason, 'no such container')) { // no such container 89 | this.log.debug({ container_id: this.json.uuid }, 'Attempting to remove a container that does not exist, continuing without error.'); 90 | return callback(); 91 | } else { 92 | return callback(err); 93 | } 94 | }); 95 | }], 96 | // Delete the configuration files for this server 97 | delete_config: ['clear_object', (r, callback) => { 98 | this.log.debug('Attempting to remove configuration files...'); 99 | 100 | let pathToRemove = Path.join('./config/servers', this.json.uuid); 101 | if (Fs.existsSync(Path.join('./config/servers', this.json.uuid, 'install.log'))) { 102 | this.log.debug('Not removing entire configuration folder because an installation log exists.'); 103 | pathToRemove = Path.join('./config/servers', this.json.uuid, 'server.json'); 104 | } 105 | 106 | Fs.remove(pathToRemove, err => { 107 | if (!err) this.log.debug('Removed configuration folder.'); 108 | return callback(); 109 | }); 110 | }], 111 | delete_folder: ['clear_object', (r, callback) => { 112 | Fs.remove(Path.join(Config.get('sftp.path', '/srv/daemon-data'), this.json.uuid), callback); 113 | }], 114 | }, err => { 115 | if (err) Log.fatal(err); 116 | if (!err) this.log.info('Server deleted.'); 117 | 118 | return next(err); 119 | }); 120 | } 121 | } 122 | 123 | module.exports = Delete; 124 | -------------------------------------------------------------------------------- /src/controllers/fs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const Fs = require('fs-extra'); 26 | const Async = require('async'); 27 | const Path = require('path'); 28 | const Chokidar = require('chokidar'); 29 | const _ = require('lodash'); 30 | const Mmm = require('mmmagic'); 31 | const RandomString = require('randomstring'); 32 | const Process = require('child_process'); 33 | const Util = require('util'); 34 | const rfr = require('rfr'); 35 | 36 | const Magic = Mmm.Magic; 37 | const Mime = new Magic(Mmm.MAGIC_MIME_TYPE | Mmm.MAGIC_SYMLINK); 38 | 39 | const ConfigHelper = rfr('src/helpers/config.js'); 40 | const Config = new ConfigHelper(); 41 | 42 | class FileSystem { 43 | constructor(server) { 44 | this.server = server; 45 | 46 | const Watcher = Chokidar.watch(this.server.configLocation, { 47 | persistent: true, 48 | awaitWriteFinish: false, 49 | }); 50 | 51 | Watcher.on('change', () => { 52 | if (this.server.knownWrite !== true) { 53 | this.server.log.debug('Detected remote file change, updating JSON object correspondingly.'); 54 | Fs.readJson(this.server.configLocation, (err, object) => { 55 | if (err) { 56 | // Try to overwrite those changes with the old config. 57 | this.server.log.warn(err, 'An error was detected with the changed file, attempting to undo the changes.'); 58 | this.server.knownWrite = true; 59 | Fs.writeJson(this.server.configLocation, this.server.json, { spaces: 2 }, writeErr => { 60 | if (!writeErr) { 61 | this.server.log.debug('Successfully undid those remote changes.'); 62 | } else { 63 | this.server.log.fatal(writeErr, 'Unable to undo those changes, this could break the daemon badly.'); 64 | } 65 | }); 66 | } else { 67 | this.server.json = object; 68 | } 69 | }); 70 | } 71 | this.server.knownWrite = false; 72 | }); 73 | } 74 | 75 | size(next) { 76 | const Exec = Process.spawn('du', ['-hsb', this.server.path()], {}); 77 | 78 | Exec.stdout.on('data', data => { 79 | next(null, parseInt(_.split(data.toString(), '\t')[0], 10)); 80 | }); 81 | 82 | Exec.on('error', execErr => { 83 | this.server.log.error(execErr); 84 | return next(new Error('There was an error while attempting to check the size of the server data folder.')); 85 | }); 86 | 87 | Exec.on('exit', (code, signal) => { 88 | if (code !== 0) { 89 | return next(new Error(`Unable to determine size of server data folder, exited with code ${code} signal ${signal}.`)); 90 | } 91 | }); 92 | } 93 | 94 | chown(file, next) { 95 | let chownTarget = file; 96 | if (!_.startsWith(chownTarget, this.server.path())) { 97 | chownTarget = this.server.path(file); 98 | } 99 | 100 | const Exec = Process.spawn('chown', ['-R', Util.format('%s:%s', Config.get('docker.container.username', 'pterodactyl'), Config.get('docker.container.username', 'pterodactyl')), chownTarget], {}); 101 | Exec.on('error', execErr => { 102 | this.server.log.error(execErr); 103 | return next(new Error('There was an error while attempting to set ownership of files.')); 104 | }); 105 | Exec.on('exit', (code, signal) => { 106 | if (code !== 0) { 107 | return next(new Error(`Unable to set ownership of files properly, exited with code ${code} signal ${signal}.`)); 108 | } 109 | return next(); 110 | }); 111 | } 112 | 113 | isSelf(moveTo, moveFrom) { 114 | const target = this.server.path(moveTo); 115 | const source = this.server.path(moveFrom); 116 | 117 | if (!_.startsWith(target, source)) { 118 | return false; 119 | } 120 | 121 | const end = target.slice(source.length); 122 | if (!end) { 123 | return true; 124 | } 125 | 126 | return _.startsWith(end, '/'); 127 | } 128 | 129 | write(file, data, next) { 130 | Async.series([ 131 | callback => { 132 | this.server.knownWrite = true; 133 | callback(); 134 | }, 135 | callback => { 136 | Fs.outputFile(this.server.path(file), data, callback); 137 | }, 138 | callback => { 139 | this.chown(file, callback); 140 | }, 141 | ], next); 142 | } 143 | 144 | read(file, next) { 145 | Fs.stat(this.server.path(file), (err, stat) => { 146 | if (err) return next(err); 147 | if (!stat.isFile()) { 148 | return next(new Error('The file requested does not appear to be a file.')); 149 | } 150 | if (stat.size > 10000000) { 151 | return next(new Error('This file is too large to open.')); 152 | } 153 | Fs.readFile(this.server.path(file), 'utf8', next); 154 | }); 155 | } 156 | 157 | readBytes(file, offset, length, next) { 158 | Fs.stat(this.server.path(file), (err, stat) => { 159 | if (err) return next(err); 160 | if (!stat.isFile()) { 161 | const internalError = new Error('Trying to read bytes from a non-file.'); 162 | internalError.code = 'EISDIR'; 163 | 164 | return next(internalError); 165 | } 166 | 167 | if (offset >= stat.size) { 168 | return next(null, null, true); 169 | } 170 | 171 | const chunks = []; 172 | const stream = Fs.createReadStream(this.server.path(file), { 173 | start: offset, 174 | end: (offset + length) - 1, 175 | }); 176 | stream.on('data', data => { 177 | chunks.push(data); 178 | }); 179 | stream.on('end', () => { 180 | next(null, Buffer.concat(chunks), false); 181 | }); 182 | }); 183 | } 184 | 185 | readEnd(file, bytes, next) { 186 | if (_.isFunction(bytes)) { 187 | next = bytes; // eslint-disable-line 188 | bytes = 80000; // eslint-disable-line 189 | } 190 | Fs.stat(this.server.path(file), (err, stat) => { 191 | if (err) return next(err); 192 | if (!stat.isFile()) { 193 | return next(new Error('The file requested does not appear to be a file.')); 194 | } 195 | let opts = {}; 196 | let lines = ''; 197 | if (stat.size > bytes) { 198 | opts = { 199 | start: (stat.size - bytes), 200 | end: stat.size, 201 | }; 202 | } 203 | const stream = Fs.createReadStream(this.server.path(file), opts); 204 | stream.on('data', data => { 205 | lines += data; 206 | }); 207 | stream.on('end', () => { 208 | next(null, lines); 209 | }); 210 | }); 211 | } 212 | 213 | mkdir(path, next) { 214 | if (!_.isArray(path)) { 215 | Fs.ensureDir(this.server.path(path), err => { 216 | if (err) return next(err); 217 | this.chown(path, next); 218 | }); 219 | } else { 220 | Async.eachOfLimit(path, 5, (value, key, callback) => { 221 | Fs.ensureDir(this.server.path(value), err => { 222 | if (err) return callback(err); 223 | this.chown(value, callback); 224 | }); 225 | }, next); 226 | } 227 | } 228 | 229 | rm(path, next) { 230 | if (_.isString(path)) { 231 | // Safety - prevent deleting the main folder. 232 | if (Path.resolve(this.server.path(path)) === this.server.path()) { 233 | return next(new Error('You cannot delete your home folder.')); 234 | } 235 | 236 | Fs.remove(this.server.path(path), next); 237 | } else { 238 | Async.eachOfLimit(path, 5, (value, key, callback) => { 239 | // Safety - prevent deleting the main folder. 240 | if (Path.resolve(this.server.path(value)) === this.server.path()) { 241 | return next(new Error('You cannot delete your home folder.')); 242 | } 243 | 244 | Fs.remove(this.server.path(value), callback); 245 | }, next); 246 | } 247 | } 248 | 249 | copy(initial, ending, opts, next) { 250 | if (_.isFunction(opts)) { 251 | next = opts; // eslint-disable-line 252 | opts = {}; // eslint-disable-line 253 | } 254 | 255 | if (!_.isArray(initial) && !_.isArray(ending)) { 256 | if (this.isSelf(ending, initial)) { 257 | return next(new Error('You cannot copy a folder into itself.')); 258 | } 259 | Async.series([ 260 | callback => { 261 | Fs.copy(this.server.path(initial), this.server.path(ending), { 262 | overwrite: opts.overwrite || true, 263 | preserveTimestamps: opts.timestamps || false, 264 | }, callback); 265 | }, 266 | callback => { 267 | this.chown(ending, callback); 268 | }, 269 | ], next); 270 | } else if (!_.isArray(initial) || !_.isArray(ending)) { 271 | return next(new Error('Values passed to copy function must be of the same type (string, string) or (array, array).')); 272 | } else { 273 | Async.eachOfLimit(initial, 5, (value, key, callback) => { 274 | if (_.isUndefined(ending[key])) { 275 | return callback(new Error('The number of starting values does not match the number of ending values.')); 276 | } 277 | 278 | if (this.isSelf(ending[key], value)) { 279 | return next(new Error('You cannot copy a folder into itself.')); 280 | } 281 | Fs.copy(this.server.path(value), this.server.path(ending[key]), { 282 | overwrite: _.get(opts, 'overwrite', true), 283 | preserveTimestamps: _.get(opts, 'timestamps', false), 284 | }, err => { 285 | if (err) return callback(err); 286 | this.chown(ending[key], callback); 287 | }); 288 | }, next); 289 | } 290 | } 291 | 292 | stat(file, next) { 293 | Fs.lstat(this.server.path(file), (err, stat) => { 294 | if (err) return next(err); 295 | Mime.detectFile(this.server.path(file), (mimeErr, result) => { 296 | next(null, { 297 | 'name': (Path.parse(this.server.path(file))).base, 298 | 'created': stat.birthtime, 299 | 'modified': stat.mtime, 300 | 'mode': stat.mode, 301 | 'size': stat.size, 302 | 'directory': stat.isDirectory(), 303 | 'file': stat.isFile(), 304 | 'symlink': stat.isSymbolicLink(), 305 | 'mime': result || 'application/octet-stream', 306 | }); 307 | }); 308 | }); 309 | } 310 | 311 | move(initial, ending, next) { 312 | if (!_.isArray(initial) && !_.isArray(ending)) { 313 | if (this.isSelf(ending, initial)) { 314 | return next(new Error('You cannot move a file or folder into itself.')); 315 | } 316 | Fs.move(this.server.path(initial), this.server.path(ending), { overwrite: false }, err => { 317 | if (err && !_.startsWith(err.message, 'EEXIST:')) return next(err); 318 | this.chown(ending, next); 319 | }); 320 | } else if (!_.isArray(initial) || !_.isArray(ending)) { 321 | return next(new Error('Values passed to move function must be of the same type (string, string) or (array, array).')); 322 | } else { 323 | Async.eachOfLimit(initial, 5, (value, key, callback) => { 324 | if (_.isUndefined(ending[key])) { 325 | return callback(new Error('The number of starting values does not match the number of ending values.')); 326 | } 327 | 328 | if (this.isSelf(ending[key], value)) { 329 | return next(new Error('You cannot move a file or folder into itself.')); 330 | } 331 | Fs.move(this.server.path(value), this.server.path(ending[key]), { overwrite: false }, err => { 332 | if (err && !_.startsWith(err.message, 'EEXIST:')) return callback(err); 333 | this.chown(ending[key], callback); 334 | }); 335 | }, next); 336 | } 337 | } 338 | 339 | decompress(files, next) { 340 | if (!_.isArray(files)) { 341 | const fromFile = this.server.path(files); 342 | const toDir = fromFile.substring(0, _.lastIndexOf(fromFile, '/')); 343 | this.systemDecompress(fromFile, toDir, next); 344 | } else if (_.isArray(files)) { 345 | Async.eachLimit(files, 1, (file, callback) => { 346 | const fromFile = this.server.path(file); 347 | const toDir = fromFile.substring(0, _.lastIndexOf(fromFile, '/')); 348 | this.systemDecompress(fromFile, toDir, callback); 349 | }, next); 350 | } else { 351 | return next(new Error('Invalid datatype passed to decompression function.')); 352 | } 353 | } 354 | 355 | systemDecompress(file, to, next) { 356 | Mime.detectFile(file, (err, result) => { 357 | if (err) return next(err); 358 | 359 | let Exec; 360 | if (result === 'application/x-gzip' || result === 'application/gzip') { 361 | Exec = Process.spawn('tar', ['xzf', Path.basename(file), '--force-local', '-C', to], { 362 | cwd: Path.dirname(file), 363 | uid: Config.get('docker.container.user', 1000), 364 | gid: Config.get('docker.container.user', 1000), 365 | }); 366 | } else if (result === 'application/zip') { 367 | Exec = Process.spawn('unzip', ['-q', '-o', Path.basename(file), '-d', to], { 368 | cwd: Path.dirname(file), 369 | uid: Config.get('docker.container.user', 1000), 370 | gid: Config.get('docker.container.user', 1000), 371 | }); 372 | } else { 373 | return next(new Error(`Decompression of file failed: ${result} is not a decompessible Mimetype.`)); 374 | } 375 | 376 | Exec.on('error', execErr => { 377 | this.server.log.error(execErr); 378 | return next(new Error('There was an error while attempting to decompress this file.')); 379 | }); 380 | Exec.on('exit', (code, signal) => { 381 | if (code !== 0) { 382 | return next(new Error(`Decompression of file exited with code ${code} signal ${signal}.`)); 383 | } 384 | 385 | return next(); 386 | }); 387 | }); 388 | } 389 | 390 | // Unlike other functions, if multiple files and folders are passed 391 | // they will all be combined into a single archive. 392 | compress(files, to, next) { 393 | if (!_.isString(to)) { 394 | return next(new Error('The to field must be a string for the folder in which the file should be saved.')); 395 | } 396 | 397 | let saveAsName = `pterodactyl.archive.${RandomString.generate(4)}.tar`; 398 | if (!_.isArray(files)) { 399 | if (this.isSelf(to, files)) { 400 | return next(new Error('Unable to compress folder into itself.')); 401 | } 402 | 403 | saveAsName = `${Path.basename(files, Path.extname(files))}.${RandomString.generate(4)}.tar`; 404 | 405 | this.systemCompress([_.replace(this.server.path(files), `${this.server.path()}/`, '')], Path.join(this.server.path(to), saveAsName), next); 406 | } else if (_.isArray(files)) { 407 | const FileEntries = []; 408 | Async.series([ 409 | callback => { 410 | Async.eachLimit(files, 5, (file, eachCallback) => { 411 | // If it is going to be inside itself, skip and move on. 412 | if (this.isSelf(to, file)) { 413 | return eachCallback(); 414 | } 415 | 416 | FileEntries.push(_.replace(this.server.path(file), `${this.server.path()}/`, '')); 417 | eachCallback(); 418 | }, callback); 419 | }, 420 | callback => { 421 | if (_.isEmpty(FileEntries)) { 422 | return next(new Error('None of the files passed to the command were valid.')); 423 | } 424 | 425 | this.systemCompress(FileEntries, Path.join(this.server.path(to), saveAsName), callback); 426 | }, 427 | ], err => { 428 | next(err, saveAsName); 429 | }); 430 | } else { 431 | return next(new Error('Invalid datatype passed to decompression function.')); 432 | } 433 | } 434 | 435 | systemCompress(files, archive, next) { 436 | const args = ['czf', archive]; 437 | _.forEach(files, file => { 438 | args.push(file); 439 | }); 440 | 441 | const Exec = Process.spawn('tar', args, { 442 | cwd: this.server.path(), 443 | uid: Config.get('docker.container.user', 1000), 444 | gid: Config.get('docker.container.user', 1000), 445 | }); 446 | 447 | Exec.on('error', execErr => { 448 | this.server.log.error(execErr); 449 | return next(new Error('There was an error while attempting to compress this folder.')); 450 | }); 451 | Exec.on('exit', (code, signal) => { 452 | if (code !== 0) { 453 | return next(new Error(`Compression of files exited with code ${code} signal ${signal}.`)); 454 | } 455 | 456 | return next(null, Path.basename(archive)); 457 | }); 458 | } 459 | 460 | directory(path, next) { 461 | const responseFiles = []; 462 | Async.waterfall([ 463 | callback => { 464 | Fs.stat(this.server.path(path), (err, s) => { 465 | if (err) return callback(err); 466 | if (!s.isDirectory()) { 467 | const error = new Error('The path requests is not a valid directory on the system.'); 468 | error.code = 'ENOENT'; 469 | return callback(error); 470 | } 471 | return callback(); 472 | }); 473 | }, 474 | callback => { 475 | Fs.readdir(this.server.path(path), callback); 476 | }, 477 | (files, callback) => { 478 | Async.each(files, (item, eachCallback) => { 479 | Async.auto({ 480 | do_stat: aCallback => { 481 | Fs.stat(Path.join(this.server.path(path), item), (statErr, stat) => { 482 | // Handle bad symlinks 483 | if (statErr && statErr.code === 'ENOENT') { 484 | return eachCallback(); 485 | } 486 | aCallback(statErr, stat); 487 | }); 488 | }, 489 | do_realpath: aCallback => { 490 | Fs.realpath(Path.join(this.server.path(path), item), (rpErr, realPath) => { 491 | aCallback(null, realPath); 492 | }); 493 | }, 494 | do_mime: aCallback => { 495 | Mime.detectFile(Path.join(this.server.path(path), item), (mimeErr, result) => { 496 | aCallback(null, result); 497 | }); 498 | }, 499 | do_push: ['do_stat', 'do_mime', 'do_realpath', (results, aCallback) => { 500 | if (!_.startsWith(results.do_realpath, this.server.path())) { 501 | return aCallback(); 502 | } 503 | 504 | responseFiles.push({ 505 | 'name': item, 506 | 'created': results.do_stat.birthtime, 507 | 'modified': results.do_stat.mtime, 508 | 'mode': results.do_stat.mode, 509 | 'size': results.do_stat.size, 510 | 'directory': results.do_stat.isDirectory(), 511 | 'file': results.do_stat.isFile(), 512 | 'symlink': results.do_stat.isSymbolicLink(), 513 | 'mime': results.do_mime || 'application/octet-stream', 514 | }); 515 | aCallback(); 516 | }], 517 | }, eachCallback); 518 | }, callback); 519 | }, 520 | ], err => { 521 | next(err, _.sortBy(responseFiles, [(o) => { return _.lowerCase(o.name); }, 'created'])); // eslint-disable-line 522 | }); 523 | } 524 | } 525 | 526 | module.exports = FileSystem; 527 | -------------------------------------------------------------------------------- /src/controllers/network.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Dockerode = require('dockerode'); 27 | const _ = require('lodash'); 28 | const CIDR = require('ip-cidr'); 29 | 30 | const Log = rfr('src/helpers/logger.js'); 31 | const LoadConfig = rfr('src/helpers/config.js'); 32 | 33 | const Config = new LoadConfig(); 34 | const DockerController = new Dockerode({ 35 | socketPath: Config.get('docker.socket', '/var/run/docker.sock'), 36 | }); 37 | 38 | const NETWORK_NAME = Config.get('docker.network.name', 'pterodactyl_nw'); 39 | 40 | class Network { 41 | // Initalization Sequence for Networking 42 | // Called when Daemon boots. 43 | init(next) { 44 | DockerController.listNetworks((err, networks) => { 45 | if (err) return next(err); 46 | const foundNetwork = _.find(networks, values => { 47 | if (values.Name === NETWORK_NAME) return values.Name; 48 | }); 49 | 50 | if (!_.isUndefined(foundNetwork)) { 51 | Log.info(`Found network interface for daemon: ${NETWORK_NAME}`); 52 | return next(); 53 | } 54 | this.buildNetwork(next); 55 | }); 56 | } 57 | 58 | // Builds the isolated network for containers. 59 | buildNetwork(next) { 60 | Log.warn('No isolated network interface for containers was detected, creating one now.'); 61 | DockerController.createNetwork({ 62 | Name: NETWORK_NAME, 63 | Driver: Config.get('docker.network.driver', 'bridge'), 64 | EnableIPv6: Config.get('docker.policy.network.ipv6', true), 65 | Internal: Config.get('docker.policy.network.internal', false), 66 | IPAM: { 67 | Config: [ 68 | { 69 | Subnet: Config.get('docker.network.interfaces.v4.subnet', '172.18.0.0/16'), 70 | Gateway: Config.get('docker.network.interfaces.v4.gateway', '172.18.0.1'), 71 | }, 72 | { 73 | Subnet: Config.get('docker.network.interfaces.v6.subnet', 'fdba:17c8:6c94::/64'), 74 | Gateway: Config.get('docker.network.interfaces.v6.gateway', 'fdba:17c8:6c94::1011'), 75 | }, 76 | ], 77 | }, 78 | Options: { 79 | 'encryption': Config.get('docker.policy.network.encryption', 'false'), 80 | 'com.docker.network.bridge.default_bridge': 'false', 81 | 'com.docker.network.bridge.enable_icc': Config.get('docker.policy.network.enable_icc', 'true'), 82 | 'com.docker.network.bridge.enable_ip_masquerade': Config.get('docker.policy.network.enable_ip_masquerade', 'true'), 83 | 'com.docker.network.bridge.host_binding_ipv4': '0.0.0.0', 84 | 'com.docker.network.bridge.name': 'pterodactyl0', 85 | 'com.docker.network.driver.mtu': '1500', 86 | }, 87 | }, err => { 88 | if (err) return next(err); 89 | Log.info(`Successfully created new network (${NETWORK_NAME}) on pterodactyl0 for isolated containers.`); 90 | return next(); 91 | }); 92 | } 93 | 94 | interface(next) { 95 | const DockerNetwork = DockerController.getNetwork(NETWORK_NAME); 96 | DockerNetwork.inspect((err, data) => { 97 | if (err) return next(err); 98 | 99 | if (_.get(data, 'Driver') === 'host') { 100 | Log.warn('Detected daemon configuation using HOST NETWORK for server containers. This can expose the host network stack to programs running in containers!'); 101 | Log.info('Gateway detected as 127.0.0.1 - using host network.'); 102 | Config.modify({ 103 | docker: { 104 | interface: '127.0.0.1', 105 | }, 106 | }, next); 107 | return; 108 | } 109 | 110 | if (_.get(data, 'Driver') === 'overlay') { 111 | Log.info('Detected daemon configuation using OVERLAY NETWORK for server containers.'); 112 | Log.warn('Removing interface address and enabling ispn.'); 113 | Config.modify({ 114 | docker: { 115 | interface: '', 116 | network: { 117 | ispn: true, 118 | }, 119 | }, 120 | }, next); 121 | return; 122 | } 123 | 124 | if (_.get(data, 'Driver') === 'weavemesh') { 125 | Log.info('Detected daemon configuation using WEAVEMESH NETWORK for server containers.'); 126 | Log.warn('Removing interface address and enabling ispn.'); 127 | Config.modify({ 128 | docker: { 129 | interface: '', 130 | network: { 131 | ispn: true, 132 | }, 133 | }, 134 | }, next); 135 | return; 136 | } 137 | 138 | if (!_.get(data, 'IPAM.Config[0].Gateway', false)) { 139 | return next(new Error('No gateway could be found for pterodactyl0.')); 140 | } 141 | 142 | const Gateway = new CIDR(_.get(data, 'IPAM.Config[0].Gateway', '172.18.0.1')); 143 | let IPGateway = null; 144 | if (!Gateway.isValid()) { 145 | return next(new Error('The pterodactyl0 network gateway is invalid.')); 146 | } 147 | 148 | const GatewayRange = Gateway.toRange(); 149 | if (GatewayRange[0] === GatewayRange[1]) { 150 | IPGateway = GatewayRange[0]; 151 | } else { 152 | const Split = _.split(GatewayRange[0], '.'); 153 | Split[3] = Number(_.last(Split)) + 1; 154 | IPGateway = Split.join('.'); 155 | } 156 | 157 | Log.info(`Networking gateway detected as ${IPGateway} for interface: pterodactyl0.`); 158 | Config.modify({ 159 | docker: { 160 | interface: IPGateway, 161 | }, 162 | }, next); 163 | }); 164 | } 165 | } 166 | 167 | module.exports = Network; 168 | -------------------------------------------------------------------------------- /src/controllers/option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Dockerode = require('dockerode'); 27 | const Async = require('async'); 28 | const Request = require('request'); 29 | const Fs = require('fs-extra'); 30 | const Path = require('path'); 31 | const Util = require('util'); 32 | const _ = require('lodash'); 33 | const isStream = require('isstream'); 34 | const createOutputStream = require('create-output-stream'); 35 | 36 | const ConfigHelper = rfr('src/helpers/config.js'); 37 | const ImageHelper = rfr('src/helpers/image.js'); 38 | 39 | const Config = new ConfigHelper(); 40 | const DockerController = new Dockerode({ 41 | socketPath: Config.get('docker.socket', '/var/run/docker.sock'), 42 | }); 43 | 44 | class Option { 45 | constructor(server) { 46 | this.server = server; 47 | this.processLogger = undefined; 48 | } 49 | 50 | pull(next) { 51 | this.server.log.debug('Contacting panel to determine scripts to run for option processes.'); 52 | 53 | const endpoint = `${Config.get('remote.base')}/api/remote/scripts/${this.server.json.uuid}`; 54 | Request({ 55 | method: 'GET', 56 | url: endpoint, 57 | timeout: 5000, 58 | headers: { 59 | 'Accept': 'application/vnd.pterodactyl.v1+json', 60 | 'Authorization': `Bearer ${Config.get('keys.0')}`, 61 | }, 62 | }, (err, resp) => { 63 | if (err) return next(err); 64 | if (resp.statusCode !== 200) { 65 | const error = new Error('Recieved a non-200 error code when attempting to check scripts for server.'); 66 | error.meta = { 67 | code: resp.statusCode, 68 | requestUrl: endpoint, 69 | }; 70 | return next(error); 71 | } 72 | 73 | const Results = JSON.parse(resp.body); 74 | return next(null, Results); 75 | }); 76 | } 77 | 78 | install(next) { 79 | this.server.log.info('Blocking server boot until option installation process is completed.'); 80 | this.server.blockBooting = true; 81 | 82 | Async.auto({ 83 | details: callback => { 84 | this.server.log.debug('Contacting remote server to pull scripts to be used.'); 85 | this.pull(callback); 86 | }, 87 | write_file: ['details', (results, callback) => { 88 | if (_.isNil(_.get(results.details, 'scripts.install', null))) { 89 | // No script defined, skip the rest. 90 | const error = new Error('No installation script was defined for this egg, skipping rest of process.'); 91 | error.code = 'E_NOSCRIPT'; 92 | return callback(error); 93 | } 94 | 95 | this.server.log.debug('Writing temporary file to be handed into the Docker container.'); 96 | Fs.outputFile(Path.join('/tmp/pterodactyl/', this.server.json.uuid, '/install.sh'), results.details.scripts.install, { 97 | mode: 0o644, 98 | encoding: 'utf8', 99 | }, callback); 100 | }], 101 | image: ['write_file', (results, callback) => { 102 | const PullImage = _.get(results.details, 'config.container', 'alpine:3.4'); 103 | // Skip local images. 104 | if (_.startsWith(PullImage, '~')) { 105 | this.server.log.debug(`Skipping pull attempt for ${_.trimStart(PullImage, '~')} as it is marked as a local image.`); 106 | return callback(); 107 | } 108 | this.server.log.debug(`Pulling ${PullImage} image if it is not already on the system.`); 109 | ImageHelper.pull(PullImage, callback); 110 | }], 111 | close_stream: ['write_file', (results, callback) => { 112 | if (isStream.isWritable(this.processLogger)) { 113 | this.processLogger.close(); 114 | this.processLogger = undefined; 115 | return callback(); 116 | } 117 | return callback(); 118 | }], 119 | setup_stream: ['close_stream', (results, callback) => { 120 | const LoggingLocation = Path.join(this.server.configDataLocation, 'install.log'); 121 | this.server.log.info({ file: LoggingLocation }, 'Writing output of installation process to file.'); 122 | this.processLogger = createOutputStream(LoggingLocation, { 123 | mode: 0o644, 124 | defaultEncoding: 'utf8', 125 | }); 126 | return callback(); 127 | }], 128 | suspend: ['image', (results, callback) => { 129 | this.server.log.info('Temporarily suspending server to avoid mishaps...'); 130 | this.server.suspend(callback); 131 | }], 132 | run: ['setup_stream', 'image', (results, callback) => { 133 | this.server.log.debug('Running privileged docker container to perform the installation process.'); 134 | 135 | const environment = []; 136 | environment.push(`SERVER_MEMORY=${this.server.json.build.memory}`); 137 | environment.push(`SERVER_IP=${this.server.json.build.default.ip}`); 138 | environment.push(`SERVER_PORT=${this.server.json.build.default.port}`); 139 | _.forEach(_.get(results.details, 'env', []), (value, key) => { 140 | environment.push(`${key}=${value}`); 141 | }); 142 | 143 | DockerController.run(_.trimStart(_.get(results.details, 'config.container', 'alpine:3.4'), '~'), [_.get(results.details, 'config.entry', 'ash'), '/mnt/install/install.sh'], (Config.get('logger.level', 'info') === 'debug') ? process.stdout : this.processLogger, { 144 | Tty: true, 145 | AttachStdin: true, 146 | AttachStdout: true, 147 | AttachStderr: true, 148 | Env: environment, 149 | Mounts: [ 150 | { 151 | Source: this.server.path(), 152 | Destination: '/mnt/server', 153 | RW: true, 154 | }, 155 | { 156 | Source: Path.join('/tmp/pterodactyl/', this.server.json.uuid), 157 | Destination: '/mnt/install', 158 | RW: true, 159 | }, 160 | ], 161 | HostConfig: { 162 | Privileged: _.get(results.details, 'scripts.privileged', false), 163 | Binds: [ 164 | Util.format('%s:/mnt/server', this.server.path()), 165 | Util.format('%s:/mnt/install', Path.join('/tmp/pterodactyl/', this.server.json.uuid)), 166 | ], 167 | NetworkMode: Config.get('docker.network.name', 'pterodactyl_nw'), 168 | }, 169 | }, (err, data, container) => { 170 | if (_.isObject(container) && _.isFunction(_.get(container, 'remove', null))) { 171 | container.remove(); 172 | } 173 | 174 | if (err) { 175 | return callback(err); 176 | } 177 | 178 | if (data.StatusCode !== 0) { 179 | return callback(new Error(`Install script failed with code ${data.StatusCode}`)); 180 | } 181 | 182 | this.server.log.info('Completed installation process for server.'); 183 | this.server.blockBooting = false; 184 | callback(err, data); 185 | }); 186 | }], 187 | close_logger: ['run', (results, callback) => { 188 | if (isStream.isWritable(this.processLogger)) { 189 | this.processLogger.close(); 190 | this.processLogger = undefined; 191 | } 192 | return callback(); 193 | }], 194 | remove_install_script: ['run', (results, callback) => { 195 | Fs.unlink(Path.join('/tmp/pterodactyl/', this.server.json.uuid, '/install.sh'), callback); 196 | }], 197 | chown: ['run', (results, callback) => { 198 | this.server.log.debug('Properly chowning all server files and folders after installation.'); 199 | this.server.fs.chown('/', callback); 200 | }], 201 | }, err => { 202 | this.server.unsuspend(() => { _.noop(); }); 203 | 204 | // No script, no need to kill everything. 205 | if (err && err.code === 'E_NOSCRIPT') { 206 | this.server.log.info(err.message); 207 | return next(); 208 | } 209 | 210 | return next(err); 211 | }); 212 | } 213 | } 214 | 215 | module.exports = Option; 216 | -------------------------------------------------------------------------------- /src/controllers/pack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const _ = require('lodash'); 27 | const Async = require('async'); 28 | const Fs = require('fs-extra'); 29 | const Cache = require('memory-cache'); 30 | const Request = require('request'); 31 | const Path = require('path'); 32 | const Crypto = require('crypto'); 33 | const Process = require('child_process'); 34 | 35 | const Log = rfr('src/helpers/logger.js'); 36 | const ConfigHelper = rfr('src/helpers/config.js'); 37 | const Config = new ConfigHelper(); 38 | 39 | class Pack { 40 | constructor(server) { 41 | this.server = server; 42 | this.pack = this.server.json.service.pack; 43 | this.archiveLocation = null; 44 | this.logger = Log; 45 | this.packdata = null; 46 | } 47 | 48 | // Called when a server is started and marked as needing a pack update 49 | // of some sort. Either due to being created, or a button click 50 | // from the panel or API that asks for an update. 51 | install(next) { 52 | if (_.isNil(this.pack) || _.isUndefined(this.pack)) { 53 | return next(); 54 | } 55 | 56 | this.archiveLocation = Path.join(Config.get('pack.cache', './packs'), this.pack, 'archive.tar.gz'); 57 | this.logger = Log.child({ pack: this.pack, server: this.server.json.uuid }); 58 | 59 | Async.series([ 60 | callback => this.checkCache(callback), 61 | callback => this.unpackToServer(callback), 62 | callback => this.server.setPermissions(callback), 63 | ], next); 64 | } 65 | 66 | checkCache(next) { 67 | // If pack is updating, just call this function every 68 | // second until it is done and then move on. 69 | if (!_.isNil(Cache.get(`pack.updating.${this.pack}`))) { 70 | setTimeout(() => { 71 | this.checkCache(next); 72 | }, 1000); 73 | return; 74 | } 75 | 76 | Cache.put(`pack.updating.${this.pack}`, true); 77 | this.logger.debug('Checking if pack needs to be updated.'); 78 | Async.auto({ 79 | file_exists: callback => { 80 | Fs.access(this.archiveLocation, (Fs.constants || Fs).R_OK, err => { 81 | if (err && err.code === 'ENOENT') { 82 | return callback(null, false); 83 | } 84 | return callback(err, true); 85 | }); 86 | }, 87 | local_hash: ['file_exists', (results, callback) => { 88 | if (!results.file_exists) return callback(); 89 | 90 | this.logger.debug('Checking existing pack checksum.'); 91 | const ChecksumStream = Fs.createReadStream(this.archiveLocation); 92 | const SHA1Hash = Crypto.createHash('sha1'); 93 | ChecksumStream.on('data', data => { 94 | SHA1Hash.update(data, 'utf8'); 95 | }); 96 | 97 | ChecksumStream.on('end', () => { 98 | Cache.put(`pack.${this.pack}`, SHA1Hash.digest('hex')); 99 | return callback(); 100 | }); 101 | 102 | ChecksumStream.on('error', callback); 103 | }], 104 | remote_hash: ['file_exists', (results, callback) => { 105 | if (!results.file_exists) return callback(); 106 | 107 | this.logger.debug('Checking remote host for valid pack checksum.'); 108 | const endpoint = `${Config.get('remote.base')}/daemon/packs/pull/${this.pack}/hash`; 109 | Request({ 110 | method: 'GET', 111 | url: endpoint, 112 | headers: { 113 | 'X-Access-Node': Config.get('keys.0'), 114 | }, 115 | }, (err, resp) => { 116 | if (err) { 117 | const error = new Error('Recieved a non-200 error code while attempting to pull the hash for a egg pack.'); 118 | error.responseCode = resp.statusCode; 119 | error.requestURL = endpoint; 120 | error.meta = err; 121 | return callback(error); 122 | } 123 | 124 | if (resp.statusCode !== 200) { 125 | return callback({ 126 | code: resp.statusCode, 127 | body: resp.body, 128 | pack: this.pack, 129 | }); 130 | } 131 | 132 | const Results = JSON.parse(resp.body); 133 | return callback(null, Results['archive.tar.gz']); 134 | }); 135 | }], 136 | }, (err, results) => { 137 | if (err) { 138 | this.logger.error(err, `Recieved a non-200 error code (${err.code}) when attempting to check a pack hash (${err.pack}).`); 139 | 140 | return next(err); 141 | } 142 | 143 | if (results.file_exists) { 144 | if (Cache.get(`pack.${this.pack}`) === results.remote_hash) { 145 | // Pack exists, and is valid. 146 | this.logger.debug('Pack checksums are valid, not re-downloading.'); 147 | Cache.del(`pack.updating.${this.pack}`); 148 | return next(); 149 | } 150 | } 151 | 152 | Log.debug('Pack was not found on the system, or the hash was different. Downloading again.'); 153 | if (results.file_exists) { 154 | this.logger.debug('Removing old pack from the system...'); 155 | Fs.unlink(Path.join(Config.get('pack.cache', './packs'), this.pack, 'archive.tar.gz'), unlinkErr => { 156 | if (unlinkErr) return next(unlinkErr); 157 | this.downloadPack(next); 158 | }); 159 | } else { 160 | this.downloadPack(next); 161 | } 162 | }); 163 | } 164 | 165 | downloadPack(next) { 166 | // If no update is in progress, this function should 167 | // contact the panel and determine if the hash has changed. 168 | // If not, simply return and tell the checkCache() call that 169 | // eveything is good. If hash has changed, handle the update. 170 | // 171 | // Will need to run the request and hash check in parallel for speed. 172 | // Should compare the returned MD5 hash to the one we have stored. 173 | Async.series([ 174 | callback => { 175 | Fs.ensureDir(Path.join(Config.get('pack.cache', './packs'), this.pack), callback); 176 | }, 177 | callback => { 178 | Log.debug('Downloading pack...'); 179 | const endpoint = `${Config.get('remote.base')}/daemon/packs/pull/${this.pack}`; 180 | 181 | Request({ method: 'GET', url: endpoint, headers: { 'X-Access-Node': Config.get('keys.0') } }) 182 | .on('error', error => { 183 | callback(error); 184 | }) 185 | .on('response', response => { 186 | if (response.statusCode !== 200) { 187 | const error = new Error('Recieved a non-200 error code while attempting to pull the hash for a egg pack.'); 188 | error.responseCode = response.statusCode; 189 | error.requestURL = endpoint; 190 | error.pack = this.pack; 191 | return callback(error); 192 | } 193 | }) 194 | .pipe(Fs.createWriteStream(this.archiveLocation)) 195 | .on('close', callback); 196 | }, 197 | callback => { 198 | Log.debug('Generating checksum...'); 199 | const ChecksumStream = Fs.createReadStream(this.archiveLocation); 200 | const SHA1Hash = Crypto.createHash('sha1'); 201 | ChecksumStream.on('data', data => { 202 | SHA1Hash.update(data, 'utf8'); 203 | }); 204 | 205 | ChecksumStream.on('end', () => { 206 | Cache.put(`pack.${this.pack}`, SHA1Hash.digest('hex')); 207 | return callback(); 208 | }); 209 | }, 210 | callback => { 211 | Log.debug('Download complete, moving on.'); 212 | Cache.del(`pack.updating.${this.pack}`); 213 | return callback(); 214 | }, 215 | ], next); 216 | } 217 | 218 | unpackToServer(next) { 219 | this.logger.debug('Unpacking pack to server.'); 220 | 221 | const Exec = Process.spawn('tar', ['-xzf', Path.basename(this.archiveLocation), '-C', this.server.path()], { 222 | cwd: Path.dirname(this.archiveLocation), 223 | stdio: ['ignore', 'ignore', 'pipe'], 224 | }); 225 | 226 | const stderrLines = []; 227 | Exec.stderr.setEncoding('utf8'); 228 | Exec.stderr.on('data', data => stderrLines.push(data)); 229 | 230 | Exec.on('error', execErr => { 231 | this.logger.error({ location: this.archiveLocation }, execErr); 232 | return next(new Error('There was an error while attempting to decompress this file.')); 233 | }); 234 | 235 | Exec.on('exit', (code, signal) => { 236 | if (code !== 0) { 237 | this.logger.error({ location: this.archiveLocation, code, signal }, `Failed to decompress server pack archive: ${stderrLines.join('\n')}`); 238 | } 239 | 240 | return next(); 241 | }); 242 | } 243 | } 244 | 245 | module.exports = Pack; 246 | -------------------------------------------------------------------------------- /src/controllers/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Async = require('async'); 27 | const Request = require('request'); 28 | const Util = require('util'); 29 | const Fs = require('fs-extra'); 30 | const Mime = require('mime'); 31 | const Path = require('path'); 32 | const Crypto = require('crypto'); 33 | const _ = require('lodash'); 34 | const Os = require('os'); 35 | const Cache = require('memory-cache'); 36 | 37 | const Status = require('./../helpers/status'); 38 | const ConfigHelper = require('./../helpers/config'); 39 | const ResponseHelper = require('./../helpers/responses'); 40 | const BuilderController = require('./../controllers/builder'); 41 | const DeleteController = require('./../controllers/delete'); 42 | const Log = require('./../helpers/logger'); 43 | const Package = require('./../../package'); 44 | 45 | const Config = new ConfigHelper(); 46 | 47 | class RouteController { 48 | constructor(auth, req, res) { 49 | this.req = req; 50 | this.res = res; 51 | 52 | this.auth = auth; 53 | this.responses = new ResponseHelper(req, res); 54 | } 55 | 56 | // Returns Index 57 | getIndex() { 58 | this.auth.allowed('c:info', (allowedErr, isAllowed) => { 59 | if (allowedErr || !isAllowed) return; 60 | 61 | this.res.send({ 62 | name: 'Pterodactyl Management Daemon', 63 | version: Package.version, 64 | system: { 65 | type: Os.type(), 66 | arch: Os.arch(), 67 | platform: Os.platform(), 68 | release: Os.release(), 69 | cpus: Os.cpus().length, 70 | freemem: Os.freemem(), 71 | }, 72 | network: Os.networkInterfaces(), 73 | }); 74 | }); 75 | } 76 | 77 | // Revoke an authentication key on demand 78 | revokeKey() { 79 | this.auth.allowed('c:revoke-key', (allowedErr, isAllowed) => { 80 | if (allowedErr || !isAllowed) return; 81 | 82 | const key = _.get(this.req.params, 'key'); 83 | Log.debug({ token: key }, 'Revoking authentication token per manual request.'); 84 | Cache.del(`auth:token:${key}`); 85 | 86 | return this.responses.generic204(null); 87 | }); 88 | } 89 | 90 | // Similar to revokeKey except it allows for multiple keys at once 91 | batchDeleteKeys() { 92 | this.auth.allowed('c:revoke-key', (allowedErr, isAllowed) => { 93 | if (allowedErr || !isAllowed) return; 94 | 95 | _.forEach(_.get(this.req.params, 'keys'), key => { 96 | Log.debug({ token: key }, 'Revoking authentication token per batch delete request.'); 97 | Cache.del(`auth:token:${key}`); 98 | }); 99 | 100 | return this.responses.generic204(null); 101 | }); 102 | } 103 | 104 | // Updates saved configuration on system. 105 | patchConfig() { 106 | this.auth.allowed('c:config', (allowedErr, isAllowed) => { 107 | if (allowedErr || !isAllowed) return; 108 | 109 | Config.modify(this.req.params, err => { 110 | this.responses.generic204(err); 111 | }); 112 | }); 113 | } 114 | 115 | // Saves Daemon Configuration to Disk 116 | putConfig() { 117 | this.auth.allowed('c:config', (allowedErr, isAllowed) => { 118 | if (allowedErr || !isAllowed) return; 119 | 120 | Config.save(this.req.params, err => { 121 | this.responses.generic204(err); 122 | }); 123 | }); 124 | } 125 | 126 | postNewServer() { 127 | this.auth.allowed('c:create', (allowedErr, isAllowed) => { 128 | if (allowedErr || !isAllowed) return; 129 | 130 | const startOnCompletion = _.get(this.req.params, 'start_on_completion', false); 131 | if (startOnCompletion) { 132 | delete this.req.params.start_on_completion; 133 | } 134 | 135 | const Builder = new BuilderController(this.req.params); 136 | 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.' }); 137 | 138 | // We sent a HTTP 202 since this might take awhile. 139 | // We do need to monitor for errors and negatiate with 140 | // the panel if they do occur. 141 | Builder.init((err, data) => { 142 | if (err) Log.fatal({ err: err, meta: _.get(err, 'meta') }, 'A fatal error was encountered while attempting to create a server.'); // eslint-disable-line 143 | 144 | const HMAC = Crypto.createHmac('sha256', Config.get('keys.0')); 145 | HMAC.update(data.uuid); 146 | 147 | Request.post(`${Config.get('remote.base')}/daemon/install`, { 148 | form: { 149 | server: data.uuid, 150 | signed: HMAC.digest('base64'), 151 | installed: (err) ? 'error' : 'installed', 152 | }, 153 | headers: { 154 | 'X-Access-Node': Config.get('keys.0'), 155 | 'Accept': 'application/json', 156 | 'Content-Type': 'application/json', 157 | }, 158 | followAllRedirects: true, 159 | timeout: 10000, 160 | }, (requestErr, response, body) => { 161 | if (requestErr || response.statusCode !== 200) { 162 | 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 }); 163 | } else { 164 | Log.info('Notified remote panel of server install status.'); 165 | } 166 | 167 | if (startOnCompletion && !err) { 168 | const Servers = rfr('src/helpers/initialize.js').Servers; 169 | Servers[data.uuid].start(startErr => { 170 | if (err) Log.error({ server: data.uuid, err: startErr }, 'There was an error while attempting to auto-start this server.'); 171 | }); 172 | } 173 | }); 174 | }); 175 | }); 176 | } 177 | 178 | getAllServers() { 179 | this.auth.allowed('c:list', (allowedErr, isAllowed) => { 180 | if (allowedErr || !isAllowed) return; 181 | 182 | const responseData = {}; 183 | Async.each(this.auth.allServers(), (server, callback) => { 184 | responseData[server.json.uuid] = { 185 | container: server.json.container, 186 | service: server.json.service, 187 | status: server.status, 188 | query: server.processData.query, 189 | proc: server.processData.process, 190 | }; 191 | callback(); 192 | }, () => { 193 | this.res.send(responseData); 194 | }); 195 | }); 196 | } 197 | 198 | deleteServer() { 199 | this.auth.allowed('g:server:delete', (allowedErr, isAllowed) => { 200 | if (allowedErr || !isAllowed) return; 201 | 202 | const Delete = new DeleteController(this.auth.server().json); 203 | Delete.delete(err => { 204 | this.responses.generic204(err); 205 | }); 206 | }); 207 | } 208 | 209 | // Handles server power 210 | putServerPower() { 211 | if (this.req.params.action === 'start') { 212 | this.auth.allowed('s:power:start', (allowedErr, isAllowed) => { 213 | if (allowedErr || !isAllowed) return; 214 | 215 | this.auth.server().start(err => { 216 | if (err && ( 217 | _.includes(err.message, 'Server is currently queued for a container rebuild') || 218 | _.includes(err.message, 'Server container was not found and needs to be rebuilt.') || 219 | _.startsWith(err.message, 'Server is already running') 220 | )) { 221 | return this.res.send(202, { 'message': err.message }); 222 | } 223 | 224 | this.responses.generic204(err); 225 | }); 226 | }); 227 | } else if (this.req.params.action === 'stop') { 228 | this.auth.allowed('s:power:stop', (allowedErr, isAllowed) => { 229 | if (allowedErr || !isAllowed) return; 230 | 231 | this.auth.server().stop(err => { 232 | this.responses.generic204(err); 233 | }); 234 | }); 235 | } else if (this.req.params.action === 'restart') { 236 | this.auth.allowed('s:power:restart', (allowedErr, isAllowed) => { 237 | if (allowedErr || !isAllowed) return; 238 | 239 | this.auth.server().restart(err => { 240 | 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.'))) { 241 | return this.res.send(202, { 'message': err.message }); 242 | } 243 | this.responses.generic204(err); 244 | }); 245 | }); 246 | } else if (this.req.params.action === 'kill') { 247 | this.auth.allowed('s:power:kill', (allowedErr, isAllowed) => { 248 | if (allowedErr || !isAllowed) return; 249 | 250 | this.auth.server().kill(err => { 251 | if (err && _.startsWith(err.message, 'Server is already stopped')) { 252 | return this.res.send(202, { 'message': err.message }); 253 | } 254 | 255 | this.responses.generic204(err); 256 | }); 257 | }); 258 | } else { 259 | this.res.send(404, { 'error': 'Unknown power action recieved.' }); 260 | } 261 | } 262 | 263 | reinstallServer() { 264 | this.auth.allowed('c:install-server', (allowedErr, isAllowed) => { 265 | if (allowedErr || !isAllowed) return; 266 | 267 | this.auth.server().reinstall(this.req.params, err => { 268 | if (err) Log.error(err); 269 | 270 | const HMAC = Crypto.createHmac('sha256', Config.get('keys.0')); 271 | HMAC.update(this.auth.serverUuid()); 272 | 273 | Request.post(`${Config.get('remote.base')}/daemon/install`, { 274 | form: { 275 | server: this.auth.serverUuid(), 276 | signed: HMAC.digest('base64'), 277 | installed: (err) ? 'error' : 'installed', 278 | }, 279 | headers: { 280 | 'X-Access-Node': Config.get('keys.0'), 281 | 'Accept': 'application/json', 282 | 'Content-Type': 'application/json', 283 | }, 284 | followAllRedirects: true, 285 | timeout: 10000, 286 | }, (requestErr, response, body) => { 287 | if (requestErr || response.statusCode !== 200) { 288 | 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 }); 289 | } else { 290 | Log.info('Notified remote panel of server install status.'); 291 | } 292 | }); 293 | }); 294 | 295 | this.res.send(202, { 'message': 'Server is being reinstalled.' }); 296 | }); 297 | } 298 | 299 | getServer() { 300 | this.auth.allowed('s:console', (allowedErr, isAllowed) => { 301 | if (allowedErr || !isAllowed) return; 302 | 303 | this.res.send({ 304 | // container: this.auth.server().json.container, 305 | // service: this.auth.server().json.service, 306 | status: this.auth.server().status, 307 | query: this.auth.server().processData.query, 308 | proc: this.auth.server().processData.process, 309 | }); 310 | }); 311 | } 312 | 313 | // Sends command to server 314 | postServerCommand() { 315 | this.auth.allowed('s:command', (allowedErr, isAllowed) => { 316 | if (allowedErr || !isAllowed) return; 317 | 318 | if (this.auth.server().status === Status.OFF) { 319 | return this.res.send(412, { 320 | 'error': 'Server is not running.', 321 | 'route': this.req.path, 322 | 'req_id': this.req.id, 323 | 'type': this.req.contentType, 324 | }); 325 | } 326 | 327 | if (!_.isUndefined(this.req.params.command)) { 328 | this.auth.server().command(this.req.params.command).then(() => { 329 | this.responses.generic204(); 330 | }).catch(err => { 331 | this.responses.generic500(err); 332 | }); 333 | } else { 334 | this.res.send(500, { 'error': 'Missing command in request.' }); 335 | } 336 | }); 337 | } 338 | 339 | // Returns listing of server files. 340 | getServerDirectory() { 341 | this.auth.allowed('s:files:get', (allowedErr, isAllowed) => { 342 | if (allowedErr || !isAllowed) return; 343 | 344 | this.auth.server().fs.directory(this.req.params[0], (err, data) => { 345 | if (err) { 346 | switch (err.code) { 347 | case 'ENOENT': 348 | return this.res.send(404); 349 | default: 350 | return this.responses.generic500(err); 351 | } 352 | } 353 | return this.res.send(data); 354 | }); 355 | }); 356 | } 357 | 358 | // Return file contents 359 | getServerFile() { 360 | this.auth.allowed('s:files:read', (allowedErr, isAllowed) => { 361 | if (allowedErr || !isAllowed) return; 362 | 363 | this.auth.server().fs.read(this.req.params[0], (err, data) => { 364 | if (err) { 365 | switch (err.code) { 366 | case 'ENOENT': 367 | return this.res.send(404); 368 | default: 369 | return this.responses.generic500(err); 370 | } 371 | } 372 | return this.res.send({ content: data }); 373 | }); 374 | }); 375 | } 376 | 377 | getServerLog() { 378 | this.auth.allowed('s:console', (allowedErr, isAllowed) => { 379 | if (allowedErr || !isAllowed) return; 380 | 381 | this.auth.server().fs.readEnd(this.auth.server().service.object.log.location, (err, data) => { 382 | if (err) { 383 | return this.responses.generic500(err); 384 | } 385 | return this.res.send(data); 386 | }); 387 | }); 388 | } 389 | 390 | getServerFileStat() { 391 | this.auth.allowed('s:files:read', (allowedErr, isAllowed) => { 392 | if (allowedErr || !isAllowed) return; 393 | 394 | this.auth.server().fs.stat(this.req.params[0], (err, data) => { 395 | if (err) { 396 | switch (err.code) { 397 | case 'ENOENT': 398 | return this.res.send(404); 399 | default: 400 | return this.responses.generic500(err); 401 | } 402 | } 403 | return this.res.send(data); 404 | }); 405 | }); 406 | } 407 | 408 | postFileFolder() { 409 | this.auth.allowed('s:files:create', (allowedErr, isAllowed) => { 410 | if (allowedErr || !isAllowed) return; 411 | 412 | this.auth.server().fs.mkdir(this.req.params.path, err => { 413 | this.responses.generic204(err); 414 | }); 415 | }); 416 | } 417 | 418 | postFileCopy() { 419 | this.auth.allowed('s:files:copy', (allowedErr, isAllowed) => { 420 | if (allowedErr || !isAllowed) return; 421 | 422 | this.auth.server().fs.copy(this.req.params.from, this.req.params.to, err => { 423 | this.responses.generic204(err); 424 | }); 425 | }); 426 | } 427 | 428 | // prevent breaking API change for now. 429 | deleteServerFile() { 430 | this.auth.allowed('s:files:delete', (allowedErr, isAllowed) => { 431 | if (allowedErr || !isAllowed) return; 432 | 433 | this.auth.server().fs.rm(this.req.params[0], err => { 434 | this.responses.generic204(err); 435 | }); 436 | }); 437 | } 438 | 439 | postFileDelete() { 440 | this.auth.allowed('s:files:delete', (allowedErr, isAllowed) => { 441 | if (allowedErr || !isAllowed) return; 442 | 443 | this.auth.server().fs.rm(this.req.params.items, err => { 444 | this.responses.generic204(err); 445 | }); 446 | }); 447 | } 448 | 449 | postFileMove() { 450 | this.auth.allowed('s:files:move', (allowedErr, isAllowed) => { 451 | if (allowedErr || !isAllowed) return; 452 | 453 | this.auth.server().fs.move(this.req.params.from, this.req.params.to, err => { 454 | this.responses.generic204(err); 455 | }); 456 | }); 457 | } 458 | 459 | postFileDecompress() { 460 | this.auth.allowed('s:files:decompress', (allowedErr, isAllowed) => { 461 | if (allowedErr || !isAllowed) return; 462 | 463 | this.auth.server().fs.decompress(this.req.params.files, err => { 464 | this.responses.generic204(err); 465 | }); 466 | }); 467 | } 468 | 469 | postFileCompress() { 470 | this.auth.allowed('s:files:compress', (allowedErr, isAllowed) => { 471 | if (allowedErr || !isAllowed) return; 472 | 473 | this.auth.server().fs.compress(this.req.params.files, this.req.params.to, (err, filename) => { 474 | if (err) { 475 | return this.responses.generic500(err); 476 | } 477 | return this.res.send({ 478 | saved_as: filename, 479 | }); 480 | }); 481 | }); 482 | } 483 | 484 | postServerFile() { 485 | this.auth.allowed('s:files:post', (allowedErr, isAllowed) => { 486 | if (allowedErr || !isAllowed) return; 487 | 488 | this.auth.server().fs.write(this.req.params.path, this.req.params.content, err => { 489 | this.responses.generic204(err); 490 | }); 491 | }); 492 | } 493 | 494 | updateServerConfig() { 495 | this.auth.allowed('g:server:patch', (allowedErr, isAllowed) => { 496 | if (allowedErr || !isAllowed) return; 497 | 498 | this.auth.server().modifyConfig(this.req.params, (this.req.method === 'PUT'), err => { 499 | this.responses.generic204(err); 500 | }); 501 | }); 502 | } 503 | 504 | rebuildServer() { 505 | this.auth.allowed('g:server:rebuild', (allowedErr, isAllowed) => { 506 | if (allowedErr || !isAllowed) return; 507 | 508 | this.auth.server().modifyConfig({ rebuild: true }, false, err => { 509 | this.responses.generic204(err); 510 | }); 511 | }); 512 | } 513 | 514 | postServerSuspend() { 515 | this.auth.allowed('g:server:suspend', (allowedErr, isAllowed) => { 516 | if (allowedErr || !isAllowed) return; 517 | 518 | this.auth.server().suspend(err => { 519 | this.responses.generic204(err); 520 | }); 521 | }); 522 | } 523 | 524 | postServerUnsuspend() { 525 | this.auth.allowed('g:server:unsuspend', (allowedErr, isAllowed) => { 526 | if (allowedErr || !isAllowed) return; 527 | 528 | this.auth.server().unsuspend(err => { 529 | this.responses.generic204(err); 530 | }); 531 | }); 532 | } 533 | 534 | downloadServerFile() { 535 | Request(`${Config.get('remote.base')}/api/remote/download-file`, { 536 | method: 'POST', 537 | json: { 538 | token: this.req.params.token, 539 | }, 540 | headers: { 541 | 'Accept': 'application/vnd.pterodactyl.v1+json', 542 | 'Authorization': `Bearer ${Config.get('keys.0')}`, 543 | }, 544 | timeout: 5000, 545 | }, (err, response, body) => { 546 | if (err) { 547 | Log.warn(err, 'Download action failed due to an error with the request.'); 548 | return this.res.send(500, { 'error': 'An error occured while attempting to perform this request.' }); 549 | } 550 | 551 | if (response.statusCode === 200) { 552 | try { 553 | const json = _.isString(body) ? JSON.parse(body) : body; 554 | if (!_.isUndefined(json) && json.path) { 555 | const Server = this.auth.allServers(); 556 | // Does the server even exist? 557 | if (_.isUndefined(Server[json.server])) { 558 | return this.res.send(404, { 'error': 'No server found for the specified resource.' }); 559 | } 560 | 561 | // Get necessary information for the download. 562 | const Filename = Path.basename(json.path); 563 | const Mimetype = Mime.getType(json.path); 564 | const File = Server[json.server].path(json.path); 565 | const Stat = Fs.statSync(File); 566 | if (!Stat.isFile()) { 567 | return this.res.send(404, { 'error': 'Could not locate the requested file.' }); 568 | } 569 | 570 | this.res.writeHead(200, { 571 | 'Content-Type': Mimetype, 572 | 'Content-Length': Stat.size, 573 | 'Content-Disposition': Util.format('attachment; filename=%s', Filename), 574 | }); 575 | const Filestream = Fs.createReadStream(File); 576 | Filestream.pipe(this.res); 577 | } else { 578 | return this.res.send(424, { 'error': 'The upstream response did not include a valid download path.' }); 579 | } 580 | } catch (ex) { 581 | Log.error(ex); 582 | return this.res.send(500, { 'error': 'An unexpected error occured while attempting to process this request.' }); 583 | } 584 | } else { 585 | if (response.statusCode >= 500) { 586 | Log.warn({ res_code: response.statusCode, res_body: body }, 'An error occured while attempting to retrieve file download information for an upstream provider.'); 587 | } 588 | 589 | this.res.redirect(this.req.header('Referer') || Config.get('remote.base'), _.constant('')); 590 | } 591 | }); 592 | } 593 | } 594 | 595 | module.exports = RouteController; 596 | -------------------------------------------------------------------------------- /src/controllers/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Request = require('request'); 27 | const Async = require('async'); 28 | const Fs = require('fs-extra'); 29 | const Crypto = require('crypto'); 30 | const _ = require('lodash'); 31 | 32 | const Log = rfr('src/helpers/logger.js'); 33 | const ConfigHelper = rfr('src/helpers/config.js'); 34 | 35 | const Config = new ConfigHelper(); 36 | 37 | class Service { 38 | boot(next) { 39 | Async.auto({ 40 | services: callback => { 41 | Log.info('Contacting panel to retrieve a list of currrent Eggs available to the node.'); 42 | this.getServices(callback); 43 | }, 44 | compare: ['services', (results, callback) => { 45 | Log.info('Checking existing eggs against Panel response...'); 46 | 47 | const needsUpdate = []; 48 | Async.eachOf(results.services, (hash, uuid, loopCallback) => { 49 | const currentFile = `./src/services/configs/${uuid}.json`; 50 | Log.debug({ egg: uuid }, 'Checking that egg exists and is up-to-date.'); 51 | Fs.stat(currentFile, (err, stats) => { 52 | if (err && err.code === 'ENOENT') { 53 | needsUpdate.push(uuid); 54 | return loopCallback(); 55 | } else if (err) { return loopCallback(err); } 56 | 57 | if (!stats.isFile()) return loopCallback(); 58 | 59 | const currentChecksum = Crypto.createHash('sha1').update(Fs.readFileSync(currentFile), 'utf8').digest('hex'); 60 | 61 | if (currentChecksum !== hash) { 62 | needsUpdate.push(uuid); 63 | } 64 | 65 | return loopCallback(); 66 | }); 67 | }, err => { 68 | callback(err, needsUpdate); 69 | }); 70 | }], 71 | download: ['compare', (results, callback) => { 72 | if (_.isEmpty(results.compare)) return callback(); 73 | 74 | Async.each(results.compare, (uuid, eCallback) => { 75 | Log.debug({ egg: uuid }, 'Egg detected as missing or in need of update, pulling now.'); 76 | this.pullFile(uuid, eCallback); 77 | }, callback); 78 | }], 79 | }, next); 80 | } 81 | 82 | getServices(next) { 83 | const endpoint = `${Config.get('remote.base')}/api/remote/eggs`; 84 | Request({ 85 | method: 'GET', 86 | url: endpoint, 87 | headers: { 88 | 'Accept': 'application/vnd.pterodactyl.v1+json', 89 | 'Authorization': `Bearer ${Config.get('keys.0')}`, 90 | }, 91 | }, (err, response, body) => { 92 | if (err) return next(err); 93 | 94 | if (response.statusCode !== 200) { 95 | const error = new Error('Error while attempting to fetch list of Eggs from the panel.'); 96 | error.responseCode = response.statusCode; 97 | error.requestURL = endpoint; 98 | return next(error); 99 | } 100 | 101 | try { 102 | return next(null, JSON.parse(body)); 103 | } catch (ex) { 104 | return next(ex); 105 | } 106 | }); 107 | } 108 | 109 | pullFile(uuid, next) { 110 | Log.debug({ egg: uuid }, 'Retrieving an updated egg from the Panel.'); 111 | Request({ 112 | method: 'GET', 113 | url: `${Config.get('remote.base')}/api/remote/eggs/${uuid}`, 114 | headers: { 115 | 'Accept': 'application/vnd.pterodactyl.v1+json', 116 | 'Authorization': `Bearer ${Config.get('keys.0')}`, 117 | }, 118 | }, (err, response, body) => { 119 | if (err) return next(err); 120 | 121 | if (response.statusCode !== 200) { 122 | return next(new Error(`Error while attempting to fetch updated Egg file (HTTP/${response.statusCode})`)); 123 | } 124 | 125 | Log.debug({ egg: uuid }, 'Writing new egg file to filesystem.'); 126 | Fs.outputFile(`./src/services/configs/${uuid}.json`, body, next); 127 | }); 128 | } 129 | } 130 | 131 | module.exports = Service; 132 | -------------------------------------------------------------------------------- /src/errors/file_parse_error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2016 Dane Everitt 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 | */ 25 | 26 | module.exports = class FileParseError extends Error { 27 | constructor(message, file) { 28 | super(); 29 | 30 | this.code = 'PREFLIGHT_FILE_PARSE_ERROR'; 31 | this.file = file; 32 | this.message = message; 33 | 34 | Error.captureStackTrace(this, FileParseError); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/errors/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | 26 | const FileParseError = require('./file_parse_error'); 27 | const NoEggConfigurationError = require('./no_egg_config'); 28 | 29 | module.exports = { 30 | FileParseError, 31 | NoEggConfigurationError, 32 | }; 33 | -------------------------------------------------------------------------------- /src/errors/no_egg_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2016 Dane Everitt 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 | */ 25 | 26 | module.exports = class NoEggConfigurationError extends Error { 27 | constructor(...args) { 28 | super(...args); 29 | 30 | this.message = 'No egg configuration was defined for this egg.'; 31 | this.code = 'NO_EGG_CONFIGURATION'; 32 | 33 | Error.captureStackTrace(this, NoEggConfigurationError); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/helpers/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Fs = require('fs-extra'); 27 | const _ = require('lodash'); 28 | const extendify = require('extendify'); 29 | const Cache = require('memory-cache'); 30 | 31 | class Config { 32 | constructor() { 33 | if (_.isNull(Cache.get('config'))) { 34 | Cache.put('config', this.raw()); 35 | } 36 | } 37 | 38 | raw() { 39 | try { 40 | return rfr('config/core.json'); 41 | } catch (ex) { 42 | if (ex.code === 'MODULE_NOT_FOUND') { 43 | console.error('+ ------------------------------------ +'); // eslint-disable-line 44 | console.error('| No config file located for Daemon! |'); // eslint-disable-line 45 | console.error('| Please create a configuration file |'); // eslint-disable-line 46 | console.error('| at config/core.json with the info |'); // eslint-disable-line 47 | console.error('| provided by Pterodactyl Panel when |'); // eslint-disable-line 48 | console.error('| you created this node. |'); // eslint-disable-line 49 | console.error('+ ------------------------------------ +'); // eslint-disable-line 50 | console.trace(ex.message); // eslint-disable-line 51 | process.exit(1); 52 | } 53 | throw ex; 54 | } 55 | } 56 | 57 | get(key, defaultResponse) { 58 | let getObject; 59 | try { 60 | getObject = _.reduce(_.split(key, '.'), (o, i) => o[i], Cache.get('config')); 61 | } catch (ex) { _.noop(); } 62 | 63 | if (!_.isUndefined(getObject)) { 64 | return getObject; 65 | } 66 | 67 | return (!_.isUndefined(defaultResponse)) ? defaultResponse : undefined; 68 | } 69 | 70 | save(json, next) { 71 | if (!json || !_.isObject(json) || _.isNull(json) || !_.keys(json).length) { 72 | throw new Error('Invalid JSON was passed to Builder.'); 73 | } 74 | 75 | Fs.writeJson('./config/core.json', json, { spaces: 2 }, err => { 76 | if (!err) Cache.put('config', json); 77 | return next(err); 78 | }); 79 | } 80 | 81 | modify(object, next) { 82 | if (!_.isObject(object)) return next(new Error('Function expects an object to be passed.')); 83 | 84 | const deepExtend = extendify({ 85 | inPlace: false, 86 | arrays: 'replace', 87 | }); 88 | const modifiedJson = deepExtend(Cache.get('config'), object); 89 | 90 | Fs.writeJson('./config/core.json', modifiedJson, { spaces: 2 }, err => { 91 | if (err) return next(err); 92 | 93 | Cache.put('config', modifiedJson); 94 | return next(); 95 | }); 96 | } 97 | } 98 | 99 | module.exports = Config; 100 | -------------------------------------------------------------------------------- /src/helpers/fileparser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const _ = require('lodash'); 26 | const rfr = require('rfr'); 27 | const Async = require('async'); 28 | const Fs = require('fs-extra'); 29 | const Properties = require('properties-parser'); 30 | const Yaml = require('node-yaml'); 31 | const Ini = require('ini'); 32 | const Xml = require('xml2js'); 33 | 34 | const ConfigHelper = rfr('src/helpers/config.js'); 35 | 36 | const Config = new ConfigHelper(); 37 | 38 | class FileParser { 39 | constructor(server) { 40 | this.server = server; 41 | } 42 | 43 | getReplacement(replacement) { 44 | return replacement.replace(/{{\s?([\w.-]+)\s?}}/g, ($0, $1) => { // eslint-disable-line 45 | if (_.startsWith($1, 'server')) { 46 | return _.reduce(_.split(_.replace($1, 'server.', ''), '.'), (o, i) => o[i], this.server.json); 47 | } else if (_.startsWith($1, 'env')) { 48 | return _.get(this.server.json, `build.env.${_.replace($1, 'env.', '')}`, ''); 49 | } else if (_.startsWith($1, 'config')) { 50 | return Config.get(_.replace($1, 'config.', '')); 51 | } 52 | return $0; 53 | }); 54 | } 55 | 56 | file(file, strings, next) { 57 | if (!_.isObject(strings)) { 58 | return next(new Error('Variable `strings` must be passed as an object.')); 59 | } 60 | 61 | Async.waterfall([ 62 | callback => { 63 | Fs.readFile(this.server.path(file), (err, data) => { 64 | if (err) { 65 | if (_.startsWith(err.message, 'ENOENT: no such file or directory')) return next(); 66 | return next(err); 67 | } 68 | return callback(null, _.split(data.toString(), '\n')); 69 | }); 70 | }, 71 | (lines, callback) => { 72 | // @TODO: add line if its not already there. 73 | Async.forEachOf(lines, (line, index, eachCallback) => { 74 | Async.forEachOf(strings, (replaceString, findString, eachEachCallback) => { 75 | if (_.startsWith(line, findString)) { 76 | lines[index] = this.getReplacement(replaceString); // eslint-disable-line 77 | } 78 | return eachEachCallback(); 79 | }, () => { 80 | eachCallback(); 81 | }); 82 | }, () => { 83 | callback(null, lines.join('\n')); 84 | }); 85 | }, 86 | (lines, callback) => { 87 | Fs.writeFile(this.server.path(file), lines, callback); 88 | }, 89 | ], (err, result) => { 90 | next(err, result); 91 | }); 92 | } 93 | 94 | yaml(file, strings, next) { 95 | Yaml.read(this.server.path(file), (err, data) => { 96 | if (err) { 97 | if (_.startsWith(err.message, 'ENOENT: no such file or directory')) return next(); 98 | return next(err); 99 | } 100 | Async.forEachOf(strings, (replacement, eachKey, callback) => { 101 | let newValue; 102 | const matchedElements = []; 103 | 104 | // Used for wildcard matching 105 | const Split = _.split(eachKey, '.'); 106 | const Pos = _.indexOf(Split, '*'); 107 | 108 | // Determine if a '*' character is present, and if so 109 | // push all of the matching keys into the matchedElements array 110 | if (Pos >= 0) { 111 | const SearchBlock = (_.dropRight(Split, Split.length - Pos)).join('.'); 112 | _.find(data[SearchBlock], (object, key) => { // eslint-disable-line 113 | Split[Pos] = key; 114 | matchedElements.push(Split.join('.')); 115 | }); 116 | } else { 117 | matchedElements.push(eachKey); 118 | } 119 | 120 | // Loop through the matchedElements array and handle replacements 121 | // as needed. 122 | Async.each(matchedElements, (element, eCallback) => { 123 | if (_.isString(replacement)) { 124 | newValue = this.getReplacement(replacement); 125 | } else if (_.isObject(replacement)) { 126 | // Find & Replace 127 | newValue = _.get(data, element); 128 | _.forEach(replacement, (rep, find) => { 129 | newValue = _.replace(newValue, find, this.getReplacement(rep)); 130 | }); 131 | } else { 132 | newValue = replacement; 133 | } 134 | 135 | if (!_.isBoolean(newValue) && !_.isNaN(_.toNumber(newValue))) { 136 | newValue = _.toNumber(newValue); 137 | } 138 | 139 | _.set(data, element, newValue); 140 | eCallback(); 141 | }, callback); 142 | }, () => { 143 | Yaml.write(this.server.path(file), data, writeErr => { 144 | next(writeErr); 145 | }); 146 | }); 147 | }); 148 | } 149 | 150 | prop(file, strings, next) { 151 | Properties.createEditor(this.server.path(file), (err, Editor) => { 152 | if (err) { 153 | if (_.startsWith(err.message, 'ENOENT: no such file or directory')) return next(); 154 | return next(err); 155 | } 156 | Async.forEachOf(strings, (value, key, callback) => { 157 | let newValue; 158 | if (_.isString(value)) { 159 | newValue = this.getReplacement(value); 160 | } else { newValue = value; } 161 | 162 | Editor.set(key, newValue); 163 | callback(); 164 | }, () => { 165 | Editor.save(this.server.path(file), next); 166 | }); 167 | }); 168 | } 169 | 170 | json(file, strings, next) { 171 | Fs.readJson(this.server.path(file), (err, data) => { 172 | if (err) { 173 | if (_.startsWith(err.message, 'ENOENT: no such file or directory')) return next(); 174 | return next(err); 175 | } 176 | Async.forEachOf(strings, (replacement, eachKey, callback) => { 177 | let newValue; 178 | const matchedElements = []; 179 | 180 | // Used for wildcard matching 181 | const Split = _.split(eachKey, '.'); 182 | const Pos = _.indexOf(Split, '*'); 183 | 184 | // Determine if a '*' character is present, and if so 185 | // push all of the matching keys into the matchedElements array 186 | if (Pos >= 0) { 187 | const SearchBlock = (_.dropRight(Split, Split.length - Pos)).join('.'); 188 | _.find(data[SearchBlock], (object, key) => { // eslint-disable-line 189 | Split[Pos] = key; 190 | matchedElements.push(Split.join('.')); 191 | }); 192 | } else { 193 | matchedElements.push(eachKey); 194 | } 195 | 196 | // Loop through the matchedElements array and handle replacements 197 | // as needed. 198 | Async.each(matchedElements, (element, eCallback) => { 199 | if (_.isString(replacement)) { 200 | newValue = this.getReplacement(replacement); 201 | } else if (_.isObject(replacement)) { 202 | // Find & Replace 203 | newValue = _.get(data, element); 204 | _.forEach(replacement, (rep, find) => { 205 | newValue = _.replace(newValue, find, this.getReplacement(rep)); 206 | }); 207 | } else { 208 | newValue = replacement; 209 | } 210 | 211 | if (!_.isBoolean(newValue) && !_.isNaN(_.toNumber(newValue))) { 212 | newValue = _.toNumber(newValue); 213 | } 214 | 215 | _.set(data, element, newValue); 216 | eCallback(); 217 | }, callback); 218 | }, () => { 219 | Fs.writeJson(this.server.path(file), data, { spaces: 2 }, next); 220 | }); 221 | }); 222 | } 223 | 224 | ini(file, strings, next) { 225 | Async.waterfall([ 226 | callback => { 227 | Fs.readFile(this.server.path(file), 'utf8', (err, result) => { 228 | if (err) { 229 | if (_.startsWith(err.message, 'ENOENT: no such file or directory')) return next(); 230 | return next(err); 231 | } 232 | callback(null, result); 233 | }); 234 | }, 235 | (contents, callback) => { 236 | const data = Ini.parse(contents); 237 | Async.forEachOf(strings, (value, key, eachCallback) => { 238 | let newValue; 239 | if (_.isString(value)) { 240 | newValue = this.getReplacement(value); 241 | } else { newValue = value; } 242 | if (!_.isBoolean(newValue) && !_.isNaN(_.toNumber(newValue))) { 243 | newValue = _.toNumber(newValue); 244 | } 245 | 246 | _.set(data, key, Ini.safe(newValue)); 247 | eachCallback(); 248 | }, () => { 249 | callback(null, data); 250 | }); 251 | }, 252 | (data, callback) => { 253 | Fs.writeFile(this.server.path(file), Ini.encode(data), 'utf8', callback); 254 | }, 255 | ], next); 256 | } 257 | 258 | xml(file, strings, next, headless) { 259 | Fs.readFile(this.server.path(file), 'utf8', (err, data) => { 260 | if (err) { 261 | if (_.startsWith(err.message, 'ENOENT: no such file or directory')) return next(); 262 | return next(err); 263 | } 264 | 265 | Xml.parseString(data, (xmlErr, result) => { 266 | if (xmlErr) return next(xmlErr); 267 | Async.forEachOf(strings, (replacement, eachKey, callback) => { 268 | let newValue; 269 | const matchedElements = []; 270 | 271 | // Used for wildcard matching 272 | const Split = _.split(eachKey, '.'); 273 | const Pos = _.indexOf(Split, '*'); 274 | 275 | // Determine if a '*' character is present, and if so 276 | // push all of the matching keys into the matchedElements array 277 | if (Pos >= 0) { 278 | const SearchBlock = (_.dropRight(Split, Split.length - Pos)).join('.'); 279 | _.find(result[SearchBlock], (object, key) => { // eslint-disable-line 280 | Split[Pos] = key; 281 | matchedElements.push(Split.join('.')); 282 | }); 283 | } else { 284 | matchedElements.push(eachKey); 285 | } 286 | 287 | // Loop through the matchedElements array and handle replacements 288 | // as needed. 289 | Async.each(matchedElements, (element, eCallback) => { 290 | if (_.isString(replacement)) { 291 | newValue = this.getReplacement(replacement); 292 | } else if (_.isObject(replacement)) { 293 | // Find & Replace 294 | newValue = _.get(result, element); 295 | _.forEach(replacement, (rep, find) => { 296 | newValue = _.replace(newValue, find, this.getReplacement(rep)); 297 | }); 298 | } else { 299 | newValue = replacement; 300 | } 301 | 302 | if (!_.isBoolean(newValue) && !_.isNaN(_.toNumber(newValue))) { 303 | newValue = _.toNumber(newValue); 304 | } 305 | 306 | _.set(result, element, newValue); 307 | eCallback(); 308 | }, callback); 309 | }, () => { 310 | const Builder = new Xml.Builder({ 311 | headless: headless === true, 312 | }); 313 | Fs.writeFile(this.server.path(file), Builder.buildObject(result), 'utf8', next); 314 | }); 315 | }); 316 | }); 317 | } 318 | 319 | xmlHeadless(file, strings, next) { 320 | this.xml(file, strings, next, true); 321 | } 322 | } 323 | 324 | module.exports = FileParser; 325 | -------------------------------------------------------------------------------- /src/helpers/image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Dockerode = require('dockerode'); 27 | const _ = require('lodash'); 28 | 29 | const LoadConfig = rfr('src/helpers/config.js'); 30 | const Log = rfr('src/helpers/logger.js'); 31 | 32 | const Config = new LoadConfig(); 33 | const DockerController = new Dockerode({ 34 | socketPath: Config.get('docker.socket', '/var/run/docker.sock'), 35 | }); 36 | 37 | class DockerImage { 38 | /** 39 | * Determines if an image exists. 40 | * @return boolean 41 | */ 42 | static exists(img, next) { 43 | const Image = DockerController.getImage(img); 44 | Image.inspect(next); 45 | } 46 | 47 | /** 48 | * Pulls an image to the server. 49 | * @param string image 50 | * @param {Function} next 51 | * @return {Function} 52 | */ 53 | static pull(image, next) { 54 | let pullWithConfig = {}; 55 | if (_.isObject(Config.get('docker.registry', false))) { 56 | if (Config.get('docker.registry.key', false)) { 57 | pullWithConfig = { 58 | authconfig: { 59 | key: Config.get('docker.registry.key', ''), 60 | }, 61 | }; 62 | } else { 63 | pullWithConfig = { 64 | authconfig: { 65 | username: Config.get('docker.registry.username', ''), 66 | password: Config.get('docker.registry.password', ''), 67 | auth: Config.get('docker.registry.auth', ''), 68 | email: Config.get('docker.registry.email', ''), 69 | serveraddress: Config.get('docker.registry.serveraddress', ''), 70 | }, 71 | }; 72 | } 73 | } 74 | 75 | const shouldUseAuth = _.some(Config.get('docker.registry.images', []), i => { // eslint-disable-line 76 | if (_.endsWith(i, '*')) { 77 | return _.startsWith(image, i.substr(0, i.length - 1)); 78 | } else if (_.startsWith(i, '*')) { 79 | return _.endsWith(image, i.substr(1, i.length)); 80 | } 81 | 82 | return i === image; 83 | }); 84 | 85 | DockerController.pull(image, shouldUseAuth ? pullWithConfig : {}, (err, stream) => { 86 | if (err) return next(err); 87 | 88 | let SendOutput; 89 | let receivedError = false; 90 | stream.setEncoding('utf8'); 91 | 92 | stream.on('data', data => { 93 | if (receivedError) { 94 | return; 95 | } 96 | 97 | const j = JSON.parse(data); 98 | if (!_.isNil(_.get(j, 'error'))) { 99 | receivedError = true; 100 | 101 | if (!_.isNil(SendOutput)) { 102 | clearInterval(SendOutput); 103 | } 104 | 105 | return next(new Error(j.error)); 106 | } 107 | 108 | if (_.isNil(SendOutput)) { 109 | Log.info(`Pulling image ${image} ... this could take a few minutes.`); 110 | const TimeInterval = (Config.get('logger.level', 'info') === 'debug') ? 2 : 10; 111 | SendOutput = setInterval(() => { 112 | if (Config.get('logger.level', 'info') === 'debug') { 113 | Log.debug(`Pulling image ${image} ... this could take a few minutes.`); 114 | } else { 115 | Log.info(`Pulling image ${image} ... this could take a few minutes.`); 116 | } 117 | }, TimeInterval * 1000); 118 | } 119 | }); 120 | 121 | stream.on('end', () => { 122 | if (!_.isNil(SendOutput)) { 123 | clearInterval(SendOutput); 124 | } 125 | 126 | if (!receivedError) { 127 | return next(); 128 | } 129 | }); 130 | 131 | stream.on('error', next); 132 | }); 133 | } 134 | } 135 | 136 | module.exports = DockerImage; 137 | -------------------------------------------------------------------------------- /src/helpers/initialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Async = require('async'); 27 | const Path = require('path'); 28 | const Util = require('util'); 29 | const Fs = require('fs-extra'); 30 | const _ = require('lodash'); 31 | const Klaw = require('klaw'); 32 | 33 | const Log = rfr('src/helpers/logger.js'); 34 | const LoadConfig = rfr('src/helpers/config.js'); 35 | const Server = rfr('src/controllers/server.js'); 36 | 37 | const Config = new LoadConfig(); 38 | const Servers = {}; 39 | 40 | class Initialize { 41 | /** 42 | * Initializes all servers on the system and loads them into memory for NodeJS. 43 | * @param {Function} next [description] 44 | * @return {[type]} [description] 45 | */ 46 | init(next) { 47 | this.folders = []; 48 | Klaw('./config/servers/').on('data', data => { 49 | this.folders.push(data.path); 50 | }).on('end', () => { 51 | Async.each(this.folders, (file, callback) => { 52 | if (Path.extname(file) !== '.json') { 53 | return callback(); 54 | } 55 | 56 | Fs.readJson(file).then(json => { 57 | if (_.isUndefined(json.uuid)) { 58 | Log.warn(Util.format('Detected valid JSON, but server was missing a UUID in %s, skipping...', file)); 59 | return callback(); 60 | } 61 | 62 | const checkPath = Path.join(Config.get('sftp.path', '/srv/daemon-data'), json.uuid); 63 | Fs.stat(checkPath).then(stats => { 64 | if (!stats.isDirectory()) { 65 | Log.warn({ server: json.uuid }, 'Detected that the server data directory is not a directory.'); 66 | } 67 | 68 | this.setup(json, callback); 69 | }).catch(err => { 70 | if (err.code === 'ENOENT') { 71 | Log.warn({ err, server: json.uuid }, 'Could not locate a server data directory. Skipping initialization of server.'); 72 | return callback(); 73 | } 74 | 75 | return callback(err); 76 | }); 77 | }).catch(callback); 78 | }, next); 79 | }); 80 | } 81 | 82 | /** 83 | * Performs the setup action for a specific server. 84 | * @param {[type]} json [description] 85 | * @param {Function} next [description] 86 | * @return {[type]} [description] 87 | */ 88 | setup(json, next) { 89 | Async.series([ 90 | callback => { 91 | if (!_.isUndefined(Servers[json.uuid])) { 92 | delete Servers[json.uuid]; 93 | } 94 | 95 | Servers[json.uuid] = new Server(json, callback); 96 | }, 97 | ], err => { 98 | if (err) return next(err); 99 | 100 | Log.debug({ server: json.uuid }, 'Loaded configuration and initalized server.'); 101 | return next(null, Servers[json.uuid]); 102 | }); 103 | } 104 | 105 | /** 106 | * Sets up a server given its UUID. 107 | */ 108 | setupByUuid(uuid, next) { 109 | Fs.readJson(Util.format('./config/servers/%s/server.json', uuid), (err, object) => { 110 | if (err) return next(err); 111 | this.setup(object, next); 112 | }); 113 | } 114 | } 115 | 116 | exports.Initialize = Initialize; 117 | exports.Servers = Servers; 118 | -------------------------------------------------------------------------------- /src/helpers/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Bunyan = require('bunyan'); 27 | const Path = require('path'); 28 | 29 | const LoadConfig = rfr('src/helpers/config.js'); 30 | const Config = new LoadConfig(); 31 | 32 | const Log = Bunyan.createLogger({ 33 | name: 'wings', 34 | src: Config.get('logger.src', false), 35 | serializers: Bunyan.stdSerializers, 36 | streams: [ 37 | { 38 | level: Config.get('logger.level', 'info'), 39 | stream: process.stdout, 40 | }, 41 | { 42 | type: 'rotating-file', 43 | level: Config.get('logger.level', 'info'), 44 | path: Path.join(Config.get('logger.path', 'logs/'), 'wings.log'), 45 | period: Config.get('logger.period', '1d'), 46 | count: Config.get('logger.count', 3), 47 | }, 48 | ], 49 | }); 50 | 51 | module.exports = Log; 52 | -------------------------------------------------------------------------------- /src/helpers/responses.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | class Responses { 26 | constructor(req, res) { 27 | this.req = req; 28 | this.res = res; 29 | } 30 | 31 | generic204(err) { 32 | if (err) { 33 | return this.generic500(err); 34 | } 35 | return this.res.send(204); 36 | } 37 | 38 | generic500(err) { 39 | return this.res.send(500, { 40 | 'error': err.message, 41 | 'route': this.req.path, 42 | 'req_id': this.req.id, 43 | 'type': this.req.contentType, 44 | }); 45 | } 46 | } 47 | 48 | module.exports = Responses; 49 | -------------------------------------------------------------------------------- /src/helpers/sftpqueue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | 26 | const _ = require('lodash'); 27 | 28 | class SFTPQueue { 29 | constructor() { 30 | this.tasks = {}; 31 | this.handlers = {}; 32 | } 33 | 34 | push(location, task) { 35 | if (this.handlers[location]) { 36 | if (!_.isArray(this.tasks[location])) { 37 | this.tasks[location] = []; 38 | } 39 | 40 | this.tasks[location].push(task); 41 | } else { 42 | this.handleTask(location, task); 43 | } 44 | } 45 | 46 | handleTask(location, task) { 47 | this.handlers[location] = true; 48 | 49 | task(() => { 50 | if (_.isArray(this.tasks[location]) && this.tasks[location].length > 0) { 51 | this.handleTask(location, this.tasks[location].shift()); 52 | } else { 53 | this.handlers[location] = false; 54 | } 55 | }); 56 | } 57 | 58 | clean() { 59 | this.tasks = {}; 60 | this.handlers = {}; 61 | } 62 | } 63 | 64 | module.exports = SFTPQueue; 65 | -------------------------------------------------------------------------------- /src/helpers/status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | module.exports = { 26 | OFF: 0, 27 | ON: 1, 28 | STARTING: 2, 29 | STOPPING: 3, 30 | }; 31 | -------------------------------------------------------------------------------- /src/helpers/timezone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Async = require('async'); 27 | const Fs = require('fs-extra'); 28 | 29 | const ConfigHelper = rfr('src/helpers/config.js'); 30 | const Config = new ConfigHelper(); 31 | 32 | class TimezoneHelper { 33 | configure(next) { 34 | if (Config.get('docker.timezone_path', false) !== false) { 35 | return next(); 36 | } 37 | 38 | Async.parallel({ 39 | is_timezone: callback => { 40 | Fs.access('/etc/timezone', (Fs.constants || Fs).F_OK, err => { 41 | callback(null, (!err)); 42 | }); 43 | }, 44 | is_localtime: callback => { 45 | Fs.access('/etc/localtime', (Fs.constants || Fs).F_OK, err => { 46 | callback(null, (!err)); 47 | }); 48 | }, 49 | }, (err, results) => { 50 | if (err) return next(err); 51 | 52 | if (!results.is_timezone && !results.is_localtime) { 53 | return next(new Error('No suitable timezone file was located on the system.')); 54 | } 55 | 56 | if (results.is_timezone) { 57 | Config.modify({ docker: { timezone_path: '/etc/timezone' } }, next); 58 | } else { 59 | Config.modify({ docker: { timezone_path: '/etc/localtime' } }, next); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | module.exports = TimezoneHelper; 66 | -------------------------------------------------------------------------------- /src/http/restify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Fs = require('fs-extra'); 27 | const Restify = require('restify'); 28 | const Bunyan = require('bunyan'); 29 | const Path = require('path'); 30 | 31 | const ConfigHelper = rfr('src/helpers/config.js'); 32 | const Config = new ConfigHelper(); 33 | 34 | const RestLogger = Bunyan.createLogger({ 35 | name: 'restify.logger', 36 | serializers: Bunyan.stdSerializers, 37 | streams: [ 38 | { 39 | level: 'info', 40 | type: 'rotating-file', 41 | path: Path.join(Config.get('logger.path', 'logs/'), 'request.log'), 42 | period: '4h', 43 | count: 3, 44 | }, 45 | ], 46 | }); 47 | 48 | const RestServer = Restify.createServer({ 49 | name: 'Pterodactyl Daemon', 50 | certificate: (Config.get('web.ssl.enabled') === true) ? Fs.readFileSync(Config.get('web.ssl.certificate')) : null, 51 | key: (Config.get('web.ssl.enabled') === true) ? Fs.readFileSync(Config.get('web.ssl.key')) : null, 52 | formatters: { 53 | 'application/json': (req, res, body, callback) => { 54 | callback(null, JSON.stringify(body, null, 4)); 55 | }, 56 | }, 57 | }); 58 | 59 | RestServer.pre((req, res, next) => { 60 | // Fix Headers 61 | if ('x-access-server' in req.headers && !('X-Access-Server' in req.headers)) { 62 | req.headers['X-Access-Server'] = req.headers['x-access-server']; // eslint-disable-line 63 | } 64 | 65 | if ('x-access-token' in req.headers && !('X-Access-Token' in req.headers)) { 66 | req.headers['X-Access-Token'] = req.headers['x-access-token']; // eslint-disable-line 67 | } 68 | return next(); 69 | }); 70 | 71 | RestServer.on('after', Restify.auditLogger({ 72 | log: RestLogger, 73 | })); 74 | 75 | // Export this for Socket.io to make use of. 76 | module.exports = RestServer; 77 | -------------------------------------------------------------------------------- /src/http/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Restify = require('restify'); 27 | const Util = require('util'); 28 | 29 | const Log = rfr('src/helpers/logger.js'); 30 | const LoadConfig = rfr('src/helpers/config.js'); 31 | const AuthorizationMiddleware = rfr('src/middleware/authorizable.js'); 32 | const RestServer = rfr('src/http/restify.js'); 33 | const RouteController = rfr('src/controllers/routes.js'); 34 | 35 | const Config = new LoadConfig(); 36 | 37 | let Auth; 38 | let Routes; 39 | 40 | RestServer.use(Restify.jsonBodyParser()); 41 | RestServer.use(Restify.CORS()); // eslint-disable-line 42 | 43 | RestServer.opts(/.*/, (req, res, next) => { 44 | res.header('Access-Control-Allow-Origin', '*'); 45 | res.header('Access-Control-Allow-Methods', req.header('Access-Control-Request-Method')); 46 | res.header('Access-Control-Allow-Headers', req.header('Access-Control-Request-Headers')); 47 | res.send(200); 48 | return next(); 49 | }); 50 | 51 | RestServer.use((req, res, next) => { 52 | // Do Authentication 53 | Auth = new AuthorizationMiddleware(req.headers['X-Access-Token'], req.headers['X-Access-Server'], res); 54 | Auth.init(() => { 55 | Routes = new RouteController(Auth, req, res); 56 | return next(); 57 | }); 58 | }); 59 | 60 | RestServer.on('uncaughtException', (req, res, route, err) => { 61 | Log.fatal({ 62 | path: route.spec.path, 63 | method: route.spec.method, 64 | server: req.headers['X-Access-Server'] || null, 65 | err, 66 | }, err.message); 67 | try { 68 | return res.send(503, { 'error': 'An unhandled exception occured while attempting to process this request.' }); 69 | } catch (ex) { 70 | // Response already sent it seems. 71 | // Not even going to log it. 72 | } 73 | }); 74 | 75 | RestServer.get('/', (req, res, next) => { 76 | Routes.getIndex(); 77 | return next(); 78 | }); 79 | 80 | RestServer.get('/v1', (req, res, next) => { 81 | Routes.getIndex(); 82 | return next(); 83 | }); 84 | 85 | /** 86 | * Revoke authentication keys manually. 87 | */ 88 | RestServer.del('/v1/keys/:key', (req, res, next) => { 89 | Routes.revokeKey(); 90 | return next(); 91 | }); 92 | 93 | RestServer.post('/v1/keys/batch-delete', (req, res, next) => { 94 | Routes.batchDeleteKeys(); 95 | return next(); 96 | }); 97 | 98 | /** 99 | * Save New Configuration for Daemon; also updates the config across the program for immediate changes. 100 | */ 101 | RestServer.put('/v1/config', (req, res, next) => { 102 | Routes.putConfig(); 103 | return next(); 104 | }); 105 | 106 | RestServer.patch('/v1/config', (req, res, next) => { 107 | Routes.patchConfig(); 108 | return next(); 109 | }); 110 | 111 | /** 112 | * Big Picture Actions 113 | */ 114 | RestServer.get('/v1/servers', (req, res, next) => { 115 | Routes.getAllServers(); 116 | return next(); 117 | }); 118 | 119 | RestServer.post('/v1/servers', (req, res, next) => { 120 | Routes.postNewServer(); 121 | return next(); 122 | }); 123 | 124 | RestServer.del('/v1/servers', (req, res, next) => { 125 | Routes.deleteServer(); 126 | return next(); 127 | }); 128 | 129 | /** 130 | * Server Actions 131 | */ 132 | RestServer.get('/v1/server', (req, res, next) => { 133 | Routes.getServer(); 134 | return next(); 135 | }); 136 | 137 | RestServer.patch('/v1/server', (req, res, next) => { 138 | Routes.updateServerConfig(); 139 | return next(); 140 | }); 141 | 142 | RestServer.put('/v1/server', (req, res, next) => { 143 | Routes.updateServerConfig(); 144 | return next(); 145 | }); 146 | 147 | RestServer.post('/v1/server/reinstall', (req, res, next) => { 148 | Routes.reinstallServer(); 149 | return next(); 150 | }); 151 | 152 | RestServer.post('/v1/server/rebuild', (req, res, next) => { 153 | Routes.rebuildServer(); 154 | return next(); 155 | }); 156 | 157 | RestServer.put('/v1/server/power', (req, res, next) => { 158 | Routes.putServerPower(); 159 | return next(); 160 | }); 161 | 162 | RestServer.post('/v1/server/command', (req, res, next) => { 163 | Routes.postServerCommand(); 164 | return next(); 165 | }); 166 | 167 | RestServer.get('/v1/server/log', (req, res, next) => { 168 | Routes.getServerLog(); 169 | return next(); 170 | }); 171 | 172 | RestServer.get(/^\/v1\/server\/directory\/?(.+)*/, (req, res, next) => { 173 | Routes.getServerDirectory(); 174 | return next(); 175 | }); 176 | 177 | RestServer.post('/v1/server/file/folder', (req, res, next) => { 178 | Routes.postFileFolder(); 179 | return next(); 180 | }); 181 | 182 | RestServer.post('/v1/server/file/copy', (req, res, next) => { 183 | Routes.postFileCopy(); 184 | return next(); 185 | }); 186 | 187 | RestServer.del(/^\/v1\/server\/file\/f\/(.+)/, (req, res, next) => { 188 | Routes.deleteServerFile(); 189 | return next(); 190 | }); 191 | 192 | RestServer.post('/v1/server/file/delete', (req, res, next) => { 193 | Routes.postFileDelete(); 194 | return next(); 195 | }); 196 | 197 | RestServer.post(/^\/v1\/server\/file\/(move|rename)/, (req, res, next) => { 198 | Routes.postFileMove(); 199 | return next(); 200 | }); 201 | 202 | RestServer.post('/v1/server/file/compress', (req, res, next) => { 203 | Routes.postFileCompress(); 204 | return next(); 205 | }); 206 | 207 | RestServer.post('/v1/server/file/decompress', (req, res, next) => { 208 | Routes.postFileDecompress(); 209 | return next(); 210 | }); 211 | 212 | RestServer.get(/^\/v1\/server\/file\/stat\/(.+)/, (req, res, next) => { 213 | Routes.getServerFileStat(); 214 | return next(); 215 | }); 216 | 217 | RestServer.get(/^\/v1\/server\/file\/f\/(.+)/, (req, res, next) => { 218 | Routes.getServerFile(); 219 | return next(); 220 | }); 221 | 222 | RestServer.post('/v1/server/file/save', (req, res, next) => { 223 | Routes.postServerFile(); 224 | return next(); 225 | }); 226 | 227 | RestServer.get('/v1/server/file/download/:token', (req, res, next) => { 228 | Routes.downloadServerFile(); 229 | return next(); 230 | }); 231 | 232 | RestServer.post('/v1/server/suspend', (req, res, next) => { 233 | Routes.postServerSuspend(); 234 | return next(); 235 | }); 236 | 237 | RestServer.post('/v1/server/unsuspend', (req, res, next) => { 238 | Routes.postServerUnsuspend(); 239 | return next(); 240 | }); 241 | 242 | RestServer.listen(Config.get('web.listen', 8080), Config.get('web.host', '0.0.0.0'), () => { 243 | Log.info(Util.format( 244 | 'Pterodactyl Daemon is now listening for %s connections on %s:%s', 245 | (Config.get('web.ssl.enabled') === true) ? 'secure' : 'insecure', 246 | Config.get('web.host', '0.0.0.0'), 247 | Config.get('web.listen', 8080) 248 | )); 249 | }); 250 | -------------------------------------------------------------------------------- /src/http/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const _ = require('lodash'); 27 | const Ansi = require('ansi-escape-sequences'); 28 | 29 | const RestServer = rfr('src/http/restify.js'); 30 | const Status = rfr('src/helpers/status.js'); 31 | const Socket = require('socket.io').listen(RestServer.server); 32 | 33 | class WebSocket { 34 | constructor(server) { 35 | this.server = server; 36 | this.websocket = Socket.of(`/v1/ws/${this.server.json.uuid}`); 37 | 38 | this.websocket.use((params, next) => { 39 | if (!params.handshake.query.token) { 40 | return next(new Error('You must pass the correct handshake values.')); 41 | } 42 | 43 | this.server.hasPermission('s:console', params.handshake.query.token, (err, hasPermission) => { 44 | if (err) { 45 | return next(new Error('There was an error while attempting to validate your permissions.')); 46 | } 47 | 48 | if (!hasPermission) { 49 | return next(new Error('You do not have permission to access the socket for this server.')); 50 | } 51 | 52 | return next(); 53 | }); 54 | }); 55 | } 56 | 57 | init() { 58 | // Send Initial Status when Websocket is connected to 59 | this.websocket.on('connection', activeSocket => { 60 | activeSocket.on('send command', data => { 61 | this.server.hasPermission('s:command', activeSocket.handshake.query.token, (err, hasPermission) => { 62 | if (err || !hasPermission) { 63 | return; 64 | } 65 | 66 | if (this.server.status === Status.OFF) { 67 | return; 68 | } 69 | 70 | this.server.command(data).catch(commandError => { 71 | this.server.log.error({ err: commandError, command: data }, 'Failed to send command to server.'); 72 | }); 73 | }); 74 | }); 75 | 76 | activeSocket.on('send server log', () => { 77 | this.server.hasPermission('s:console', activeSocket.handshake.query.token, (err, hasPermission) => { 78 | if (err || !hasPermission) { 79 | return; 80 | } 81 | 82 | if (this.server.status === Status.OFF) { 83 | return; 84 | } 85 | 86 | this.server.docker.readEndOfLog(80000).then(lines => { 87 | activeSocket.emit('server log', lines); 88 | }).catch(readError => { 89 | activeSocket.emit('console', { 90 | line: `${Ansi.style.red}[Pterodactyl Daemon] An error was encountered while attempting to read the log file!`, 91 | }); 92 | 93 | return this.server.log.error(readError); 94 | }); 95 | }); 96 | }); 97 | 98 | activeSocket.on('set status', data => { 99 | switch (data) { 100 | case 'start': 101 | case 'on': 102 | case 'boot': 103 | this.server.hasPermission('s:power:start', activeSocket.handshake.query.token, (err, hasPermission) => { 104 | if (err || !hasPermission) { 105 | return; 106 | } 107 | 108 | this.server.start(() => { _.noop(); }); 109 | }); 110 | break; 111 | case 'off': 112 | case 'stop': 113 | case 'end': 114 | case 'quit': 115 | this.server.hasPermission('s:power:stop', activeSocket.handshake.query.token, (err, hasPermission) => { 116 | if (err || !hasPermission) { 117 | return; 118 | } 119 | 120 | this.server.stop(() => { _.noop(); }); 121 | }); 122 | break; 123 | case 'restart': 124 | case 'reload': 125 | this.server.hasPermission('s:power:restart', activeSocket.handshake.query.token, (err, hasPermission) => { 126 | if (err || !hasPermission) { 127 | return; 128 | } 129 | 130 | this.server.restart(() => { _.noop(); }); 131 | }); 132 | break; 133 | case 'kill': 134 | case '^C': 135 | this.server.hasPermission('s:power:kill', activeSocket.handshake.query.token, (err, hasPermission) => { 136 | if (err || !hasPermission) { 137 | return; 138 | } 139 | 140 | this.server.kill(() => { _.noop(); }); 141 | }); 142 | break; 143 | default: 144 | break; 145 | } 146 | }); 147 | 148 | activeSocket.emit('initial status', { 149 | 'status': this.server.status, 150 | }); 151 | }); 152 | 153 | // Send server output to Websocket. 154 | this.server.on('console', output => { 155 | const data = output.toString(); 156 | // Is this data even worth dealing with? 157 | if (_.replace(data, /\s+/g, '').length > 1) { 158 | this.websocket.emit('console', { 159 | 'line': data, 160 | }); 161 | } 162 | }); 163 | 164 | // Sends query response to Websocket when it is called by the daemon function. 165 | this.server.on('query', query => { 166 | this.websocket.emit('query', { 167 | query, 168 | }); 169 | }); 170 | 171 | // Sends current server information to Websocket. 172 | this.server.on('proc', data => { 173 | this.websocket.emit('proc', { 174 | data, 175 | }); 176 | }); 177 | 178 | // Sends change of server status to Websocket. 179 | this.server.on('status', data => { 180 | this.websocket.emit('status', { 181 | 'status': data, 182 | }); 183 | }); 184 | 185 | this.server.on('crashed', () => { 186 | this.websocket.emit('crashed'); 187 | }); 188 | } 189 | } 190 | 191 | exports.ServerSockets = WebSocket; 192 | exports.Socket = Socket; 193 | -------------------------------------------------------------------------------- /src/http/stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Async = require('async'); 27 | const _ = require('lodash'); 28 | 29 | const ConfigHelper = rfr('src/helpers/config.js'); 30 | const Servers = rfr('src/helpers/initialize.js').Servers; 31 | const Status = rfr('src/helpers/status.js'); 32 | const Socket = rfr('src/http/socket.js').Socket; 33 | 34 | const Config = new ConfigHelper(); 35 | 36 | class Stats { 37 | constructor() { 38 | this.statSocket = Socket.of('/v1/stats/'); 39 | this.statSocket.use((params, next) => { 40 | if (!params.handshake.query.token) { 41 | return next(new Error('You must pass the correct handshake values.')); 42 | } 43 | if (!_.isObject(Config.get('keys')) || !_.includes(Config.get('keys'), params.handshake.query.token)) { 44 | return next(new Error('Invalid handshake value passed.')); 45 | } 46 | return next(); 47 | }); 48 | } 49 | 50 | init() { 51 | setInterval(() => { 52 | this.send(); 53 | }, 2000); 54 | } 55 | 56 | send() { 57 | const responseData = {}; 58 | const statData = { 59 | memory: 0, 60 | cpu: 0, 61 | players: 0, 62 | }; 63 | Async.each(Servers, (server, callback) => { 64 | responseData[server.json.uuid] = { 65 | container: server.json.container, 66 | service: server.json.service, 67 | status: server.status, 68 | query: server.processData.query, 69 | proc: server.processData.process, 70 | }; 71 | if (server.status !== Status.OFF) { 72 | statData.memory += _.get(server.processData, 'process.memory.total', 0); 73 | statData.cpu += _.get(server.processData, 'process.cpu.total', 0); 74 | statData.players += _.get(server.processData, 'query.players.length', 0); 75 | } 76 | return callback(); 77 | }, () => { 78 | this.statSocket.emit('live-stats', { 79 | 'servers': responseData, 80 | 'stats': statData, 81 | }); 82 | }); 83 | } 84 | } 85 | 86 | module.exports = Stats; 87 | -------------------------------------------------------------------------------- /src/http/upload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const Siofu = require('socketio-file-upload'); 27 | const Fs = require('fs-extra'); 28 | const _ = require('lodash'); 29 | 30 | const ConfigHelper = rfr('src/helpers/config.js'); 31 | const Socket = rfr('src/http/socket.js').Socket; 32 | 33 | const Config = new ConfigHelper(); 34 | 35 | class Upload { 36 | constructor(server) { 37 | this.server = server; 38 | this.websocket = Socket.of(`/v1/upload/${this.server.json.uuid}`); 39 | 40 | // Standard Websocket Permissions 41 | this.websocket.use((params, next) => { 42 | if (!params.handshake.query.token) { 43 | return next(new Error('You must pass the correct handshake values.')); 44 | } 45 | 46 | this.server.hasPermission('s:files:upload', params.handshake.query.token, (err, hasPermission) => { 47 | if (err || !hasPermission) { 48 | return next(new Error('You do not have permission to upload files to this server.')); 49 | } 50 | 51 | return next(); 52 | }); 53 | }); 54 | } 55 | 56 | init() { 57 | this.websocket.on('connection', socket => { 58 | const Uploader = new Siofu(); 59 | Uploader.listen(socket); 60 | 61 | Uploader.on('start', event => { 62 | Uploader.maxFileSize = (Config.get('uploads.size_limit', 100)) * (1000 * 1000); 63 | Uploader.dir = this.server.path(event.file.meta.path); 64 | 65 | if (event.file.size > Uploader.maxFileSize) { 66 | Uploader.abort(event.file.id, socket); 67 | } 68 | }); 69 | 70 | Uploader.on('saved', event => { 71 | if (!event.file.success) { 72 | this.server.log.warn('An error was encountered while attempting to save a file (or the network operation was interrupted).', event); 73 | 74 | Fs.remove(event.file.pathName); 75 | return; 76 | } 77 | 78 | this.server.fs.chown(event.file.pathName, err => { 79 | if (err) this.server.log.warn(err); 80 | }); 81 | }); 82 | 83 | Uploader.on('error', event => { 84 | if (_.startsWith(event.memo, 'disconnect during upload') || _.startsWith(event.error.code, 'ENOENT')) return; 85 | this.server.log.error('There was an error while attempting to process a file upload.', event); 86 | }); 87 | }); 88 | } 89 | } 90 | 91 | module.exports = Upload; 92 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const https = require('https'); 26 | const rfr = require('rfr'); 27 | const Async = require('async'); 28 | const Proc = require('child_process'); 29 | const Request = require('request'); 30 | const compareVersions = require('compare-versions'); 31 | const Fs = require('fs-extra'); 32 | const _ = require('lodash'); 33 | const Keypair = require('keypair'); 34 | const Getos = require('getos'); 35 | 36 | const Log = rfr('src/helpers/logger.js'); 37 | const Package = rfr('package.json'); 38 | const ConfigHelper = rfr('src/helpers/config.js'); 39 | const Config = new ConfigHelper(); 40 | 41 | Log.info('+ ------------------------------------ +'); 42 | Log.info(`| Running Pterodactyl Daemon v${Package.version} |`); 43 | Log.info('| https://pterodactyl.io |'); 44 | Log.info('| |'); 45 | Log.info('| Copyright 2015 - 2020 Dane Everitt |'); 46 | Log.info('| and contributors |'); 47 | Log.info('+ ------------------------------------ +'); 48 | Log.info('Loading modules, this could take a few seconds.'); 49 | 50 | const NetworkController = rfr('src/controllers/network.js'); 51 | const Initializer = rfr('src/helpers/initialize.js').Initialize; 52 | const LiveStats = rfr('src/http/stats.js'); 53 | const ServiceController = rfr('src/controllers/service.js'); 54 | const TimezoneHelper = rfr('src/helpers/timezone.js'); 55 | const SftpServer = rfr('src/http/sftp.js'); 56 | 57 | const Network = new NetworkController(); 58 | const Initialize = new Initializer(); 59 | const Stats = new LiveStats(); 60 | const Service = new ServiceController(); 61 | const Timezone = new TimezoneHelper(); 62 | const Sftp = new SftpServer(); 63 | 64 | const userDefinedCAStores = Config.get('internals.ca_stores', []); 65 | if (userDefinedCAStores.length > 0) { 66 | Log.info('Found user provided CA store settings, synchronously applying those now...'); 67 | 68 | https.globalAgent.options.ca = []; 69 | _.forEach(userDefinedCAStores, store => { 70 | https.globalAgent.options.ca.push(Fs.readFileSync(store)); 71 | }); 72 | } 73 | 74 | Log.info('Modules loaded, starting Pterodactyl Daemon...'); 75 | Async.auto({ 76 | check_version: callback => { 77 | if (Package.version === '0.0.0-canary') { 78 | return callback(null, 'Pterodactyl Daemon is up-to-date running a nightly build.'); 79 | } 80 | 81 | Request.get('https://cdn.pterodactyl.io/releases/latest.json', { 82 | timeout: 5000, 83 | }, (err, response, body) => { 84 | if (err) { 85 | return callback(null, ['An error occurred while attempting to check the latest daemon release version.']); 86 | } 87 | 88 | if (response.statusCode === 200) { 89 | const json = JSON.parse(body); 90 | 91 | if (compareVersions(Package.version, json.daemon) >= 0) { 92 | return callback(null, 'Pterodactyl Daemon is up-to-date!'); 93 | } 94 | 95 | return callback(null, [ 96 | '+ ---------------------------- WARNING! ---------------------------- +', 97 | 'Pterodactyl Daemon is not up-to-date!', 98 | '', 99 | `Installed: v${Package.version}`, 100 | ` Stable: v${json.daemon}`, 101 | ` Release: https://github.com/Pterodactyl/Daemon/releases/v${json.daemon}`, 102 | '+ ------------------------------------------------------------------ +', 103 | ]); 104 | } 105 | 106 | return callback(null, ['Unable to check if this daemon is up to date! Invalid status code returned.']); 107 | }); 108 | }, 109 | check_structure: callback => { 110 | Fs.ensureDirSync('config/servers'); 111 | Fs.ensureDirSync('config/.sftp'); 112 | Fs.ensureDirSync(Config.get('filesystem.server_logs', '/tmp/pterodactyl')); 113 | 114 | callback(); 115 | }, 116 | check_tar: callback => { 117 | Proc.exec('tar --help', {}, callback); 118 | Log.debug('Tar module found on server.'); 119 | }, 120 | check_zip: callback => { 121 | Proc.exec('unzip --help', {}, callback); 122 | Log.debug('Unzip module found on server.'); 123 | }, 124 | check_sftp_rsa_key: callback => { 125 | // Support for new standalone SFTP server. 126 | if (!Config.get('sftp.enabled', true)) { 127 | Log.debug('Not creating SFTP keys, disabled internal server...'); 128 | return callback(); 129 | } 130 | 131 | Log.debug('Checking for SFTP id_rsa key...'); 132 | Fs.stat('./config/.sftp/id_rsa', err => { 133 | if (err && err.code === 'ENOENT') { 134 | Log.info('Creating keypair to use for SFTP connections.'); 135 | 136 | const pair = Keypair({ 137 | bits: Config.get('sftp.keypair.bits', 2048), 138 | e: Config.get('sftp.keypair.e', 65537), 139 | }); 140 | Async.parallel([ 141 | pcall => { 142 | Fs.outputFile('./config/.sftp/id_rsa', pair.private, { 143 | mode: 0o600, 144 | }, pcall); 145 | }, 146 | pcall => { 147 | Fs.outputFile('./config/.sftp/id_rsa.pub', pair.public, { 148 | mode: 0o600, 149 | }, pcall); 150 | }, 151 | ], callback); 152 | } else if (err) { 153 | return callback(err); 154 | } else { 155 | return callback(); 156 | } 157 | }); 158 | }, 159 | setup_sftp_user: ['check_structure', 'check_tar', 'check_zip', (r, callback) => { 160 | Log.debug('Checking if a SFTP user needs to be created and assigned to the configuration.'); 161 | Async.waterfall([ 162 | scall => { 163 | Log.debug(`Checking if user ${Config.get('docker.container.username', 'pterodactyl')} exists or needs to be created.`); 164 | Proc.exec(`cat /etc/passwd | grep ${Config.get('docker.container.username', 'pterodactyl')}`, {}, (err, stdout) => { 165 | // grep outputs exit code 1 with no output when 166 | // nothing is matched. 167 | if (err && err.code === 1 && _.isEmpty(stdout)) { 168 | return scall(null, false); 169 | } 170 | 171 | scall(err, !_.isEmpty(stdout)); 172 | }); 173 | }, 174 | (exists, scall) => { 175 | Getos((err, os) => { 176 | scall(err, exists, os); 177 | }); 178 | }, 179 | (exists, os, scall) => { 180 | if (exists) { 181 | return scall(); 182 | } 183 | 184 | let UserCommand = ''; 185 | const Username = Config.get('docker.container.username', 'pterodactyl'); 186 | 187 | switch (_.get(os, 'dist')) { 188 | case 'Alpine Linux': 189 | UserCommand = `addgroup -S ${Username} && adduser -S -D -H -G ${Username} -s /bin/false ${Username}`; 190 | break; 191 | case 'Ubuntu Linux': 192 | case 'Debian': 193 | case 'Fedora': 194 | case 'Centos': 195 | case 'RHEL': 196 | case 'Red Hat Linux': 197 | UserCommand = `useradd --system --no-create-home --shell /bin/false ${Username}`; 198 | break; 199 | default: 200 | return scall(new Error('Unable to create a pterodactyl user and group, unknown operating system.')); 201 | } 202 | 203 | Proc.exec(UserCommand, {}, err => { 204 | if (err && (!_.includes(err.message, 'already exists') || !_.includes(err.message, `user '${Username}' in use`))) { 205 | return scall(err); 206 | } 207 | 208 | return scall(); 209 | }); 210 | }, 211 | scall => { 212 | Proc.exec(`id -u ${Config.get('docker.container.username', 'pterodactyl')}`, {}, (err, stdout) => { 213 | if (err) return scall(err); 214 | 215 | Log.info(`Configuring user ${Config.get('docker.container.username', 'pterodactyl')} (id: ${stdout.replace(/[\x00-\x1F\x7F-\x9F]/g, '')}) as the owner of all server files.`); // eslint-disable-line 216 | Config.modify({ 217 | docker: { 218 | container: { 219 | user: parseInt(stdout.replace(/[\x00-\x1F\x7F-\x9F]/g, ''), 10), // eslint-disable-line 220 | }, 221 | }, 222 | }, scall); 223 | }); 224 | }, 225 | ], callback); 226 | }], 227 | setup_timezone: ['setup_sftp_user', (r, callback) => { 228 | Log.info('Configuring timezone file location...'); 229 | Timezone.configure(callback); 230 | }], 231 | check_network: ['setup_timezone', (r, callback) => { 232 | Log.info('Checking container networking environment...'); 233 | Network.init(callback); 234 | }], 235 | setup_network: ['check_network', (r, callback) => { 236 | Log.info('Ensuring correct network interface for containers...'); 237 | Network.interface(callback); 238 | }], 239 | check_services: ['setup_network', (r, callback) => { 240 | Service.boot(callback); 241 | }], 242 | init_servers: ['check_services', (r, callback) => { 243 | Log.info('Beginning server initialization process.'); 244 | Initialize.init(callback); 245 | }], 246 | configure_perms: ['init_servers', (r, callback) => { 247 | const Servers = rfr('src/helpers/initialize.js').Servers; 248 | Async.each(Servers, (Server, loopCallback) => { 249 | Server.setPermissions(err => { 250 | if (err) { 251 | Server.log.warn('Unable to assign permissions on startup for this server. Are all of the files in the correct location?'); 252 | } 253 | loopCallback(); 254 | }); 255 | }, callback); 256 | }], 257 | init_websocket: ['init_servers', (r, callback) => { 258 | Log.info('Configuring websocket for daemon stats...'); 259 | Stats.init(); 260 | return callback(); 261 | }], 262 | init_sftp: ['init_websocket', 'check_sftp_rsa_key', (r, callback) => { 263 | // Support for new standalone SFTP server. 264 | if (!Config.get('sftp.enabled', true)) { 265 | Log.debug('Not initializing SFTP server, disabled...'); 266 | return callback(); 267 | } 268 | 269 | Log.info('Configuring internal SFTP server...'); 270 | Sftp.init(callback); 271 | }], 272 | }, (err, results) => { 273 | if (err) { 274 | // Log a fatal error and exit. 275 | // We need this to initialize successfully without any errors. 276 | Log.fatal({ err, additional: err }, 'A fatal error caused the daemon to abort the startup.'); 277 | if (err.code === 'ECONNREFUSED') { 278 | Log.fatal('+ ------------------------------------ +'); 279 | Log.fatal('| Docker is not running! |'); 280 | Log.fatal('| |'); 281 | Log.fatal('| Unable to locate a suitable socket |'); 282 | Log.fatal('| at path specified in configuration. |'); 283 | Log.fatal('+ ------------------------------------ +'); 284 | } 285 | 286 | Log.error('You should forcibly quit this process (CTRL+C) and attempt to fix the issue.'); 287 | } else { 288 | rfr('src/http/routes.js'); 289 | 290 | if (!_.isUndefined(results.check_version)) { 291 | if (_.isString(results.check_version)) { 292 | Log.info(results.check_version); 293 | } else if (_.isArray(results.check_version)) { 294 | _.forEach(results.check_version, line => { Log.warn(line); }); 295 | } 296 | } 297 | } 298 | }); 299 | 300 | process.on('uncaughtException', err => { 301 | Log.fatal(err, 'A fatal error occured during an operation.'); 302 | }); 303 | 304 | process.on('SIGUSR2', () => { 305 | Log.reopenFileStreams(); 306 | }); 307 | -------------------------------------------------------------------------------- /src/middleware/authorizable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2020 Dane Everitt . 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 | */ 25 | const rfr = require('rfr'); 26 | const _ = require('lodash'); 27 | 28 | const Log = rfr('src/helpers/logger.js'); 29 | const Servers = rfr('src/helpers/initialize.js').Servers; 30 | const LoadConfig = rfr('src/helpers/config.js'); 31 | 32 | const Config = new LoadConfig(); 33 | 34 | class AuthorizationMiddleware { 35 | constructor(token, uuid, res) { 36 | this.token = token; 37 | this.uuid = uuid; 38 | this.res = res; 39 | 40 | this.masterKeys = Config.get('keys'); 41 | } 42 | 43 | init(next) { 44 | return next(); 45 | } 46 | 47 | allowed(perm, next) { 48 | if (!_.isObject(this.masterKeys)) { 49 | return next(null, false); 50 | } 51 | 52 | if (!this.token) { 53 | this.res.send(400, { 'error': 'Missing required X-Access-Token header.' }); 54 | return next(null, false); 55 | } 56 | 57 | // Master Controller; permissions not reliant on a specific server being defined. 58 | if (_.startsWith(perm, 'c:')) { 59 | if (_.includes(this.masterKeys, this.token)) { 60 | return next(null, true); 61 | } 62 | this.res.send(403, { 'error': 'You do not have permission to perform this action aganist the system.' }); 63 | return next(null, false); 64 | } 65 | 66 | // All other permissions controllers, do rely on a specific server being defined. 67 | // Both 'c:*' and 'g:*' permissions use the same permission checking, but 'g:*' permissions 68 | // require that a server header also be sent with the request. 69 | if (!this.uuid) { 70 | this.res.send(400, { 'error': 'Missing required X-Access-Server headers.' }); 71 | return next(null, false); 72 | } 73 | 74 | if (!_.isUndefined(Servers[this.uuid])) { 75 | if (_.startsWith(perm, 'g:')) { 76 | if (_.includes(this.masterKeys, this.token)) { 77 | return next(null, true); 78 | } 79 | } else if (_.startsWith(perm, 's:')) { 80 | if (Servers[this.uuid].isSuspended()) { 81 | this.res.send(403, { 'error': 'This server is suspended and cannot be accessed with this token.' }); 82 | return next(null, false); 83 | } 84 | 85 | if (_.includes(this.masterKeys, this.token)) { 86 | return next(null, true); 87 | } 88 | 89 | Servers[this.uuid].hasPermission(perm, this.token, (err, hasPermission, code) => { 90 | if (err) { 91 | Log.error(err); 92 | 93 | this.res.send(500, { 'error': 'Internal server error.' }); 94 | return next(null, false); 95 | } 96 | 97 | if (hasPermission) { 98 | return next(null, true); 99 | } 100 | 101 | if (code === 'uuidDoesNotMatch') { 102 | this.res.send(404, { 'error': 'Unable to locate the requested server for that token.' }); 103 | return next(null, false); 104 | } else if (code === 'isSuspended') { 105 | this.res.send(403, { 'error': 'This server is suspended.' }); 106 | return next(null, false); 107 | } 108 | 109 | this.res.send(403, { 'error': 'You do not have permission to perform this action for this server.' }); 110 | return next(null, false); 111 | }); 112 | } else { 113 | this.res.send(403, { 'error': 'You do not have permission to perform this action for this server.' }); 114 | return next(null, false); 115 | } 116 | } else { 117 | this.res.send(404, { 'error': 'Unknown server defined in X-Access-Server header.' }); 118 | return next(null, false); 119 | } 120 | } 121 | 122 | server() { 123 | return Servers[this.uuid]; 124 | } 125 | 126 | serverUuid() { 127 | return this.uuid; 128 | } 129 | 130 | requestToken() { 131 | return this.token; 132 | } 133 | 134 | allServers() { 135 | return Servers; 136 | } 137 | } 138 | 139 | module.exports = AuthorizationMiddleware; 140 | -------------------------------------------------------------------------------- /src/services/configs/.githold: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pterodactyl/daemon/ca642f399528495bfde22760898d6aad297d3e9e/src/services/configs/.githold -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pterodactyl - Daemon 5 | * Copyright (c) 2015 - 2016 Dane Everitt 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 | */ 25 | 26 | const Async = require('async'); 27 | const _ = require('lodash'); 28 | 29 | const { FileParseError, NoEggConfigurationError } = require('./../errors/index'); 30 | const Status = require('./../helpers/status'); 31 | const FileParserHelper = require('./../helpers/fileparser'); 32 | 33 | module.exports = class Core { 34 | /** 35 | * Create a new base service instance for a server. 36 | * 37 | * @param {Object} server 38 | * @param {Object|null} config 39 | */ 40 | constructor(server, config = null) { 41 | this.server = server; 42 | this.service = server.json.service; 43 | this.parser = new FileParserHelper(this.server); 44 | this.config = config || {}; 45 | } 46 | 47 | /** 48 | * Initialize the configuration for a given egg on the Daemon. 49 | * 50 | * @return {Promise} 51 | */ 52 | init() { 53 | return new Promise((resolve, reject) => { 54 | if (this.config.length > 0) { 55 | return resolve(); 56 | } 57 | 58 | try { 59 | this.config = require(`./configs/${this.service.egg}.json`); 60 | } catch (ex) { 61 | if (ex.code !== 'MODULE_NOT_FOUND') { 62 | return reject(ex); 63 | } 64 | } 65 | 66 | resolve(); 67 | }); 68 | } 69 | 70 | /** 71 | * Handles server preflight (things that need to happen before server boot). By default this will 72 | * iterate over all of the files to be edited for a server's egg and make any necessary modifications 73 | * to the file. 74 | * 75 | * @return {Promise} 76 | */ 77 | onPreflight() { 78 | return new Promise((resolve, reject) => { 79 | if (_.isEmpty(this.config)) { 80 | return reject(new NoEggConfigurationError()); 81 | } 82 | 83 | // Check each configuration file and set variables as needed. 84 | let lastFile; 85 | Async.forEachOf(_.get(this.config, 'configs', {}), (data, file, callback) => { 86 | lastFile = file; 87 | switch (_.get(data, 'parser', 'file')) { 88 | case 'file': 89 | this.parser.file(file, _.get(data, 'find', {}), callback); 90 | break; 91 | case 'yaml': 92 | this.parser.yaml(file, _.get(data, 'find', {}), callback); 93 | break; 94 | case 'properties': 95 | this.parser.prop(file, _.get(data, 'find', {}), callback); 96 | break; 97 | case 'ini': 98 | this.parser.ini(file, _.get(data, 'find', {}), callback); 99 | break; 100 | case 'json': 101 | this.parser.json(file, _.get(data, 'find', {}), callback); 102 | break; 103 | case 'xml': 104 | this.parser.xml(file, _.get(data, 'find', {}), callback); 105 | break; 106 | case 'xml-headless': 107 | this.parser.xmlHeadless(file, _.get(data, 'find', {}), callback); 108 | break; 109 | default: 110 | return callback(new Error('Parser assigned to file is not valid.')); 111 | } 112 | }, err => { 113 | if (err) { 114 | return reject(new FileParseError(err.message, lastFile)); 115 | } 116 | 117 | return resolve(); 118 | }); 119 | 120 | }); 121 | } 122 | 123 | /** 124 | * Process console data from a server. This function can perform a number of different operations, 125 | * but by default it will push data over the console socket, and check if the server requires 126 | * user interaction to continue with startup. 127 | * 128 | * @param {String} data 129 | */ 130 | onConsole(data) { 131 | Async.parallel([ 132 | () => { 133 | if (_.startsWith(data, '> ' || _.startsWith(data, '=> '))) { 134 | data = data.substr(2); 135 | } 136 | 137 | this.server.emit('console', data); 138 | }, 139 | () => { 140 | if (this.server.status === Status.ON) { 141 | return; 142 | } 143 | 144 | // Started 145 | if (_.includes(data, _.get(this.config, 'startup.done', null))) { 146 | this.server.setStatus(Status.ON); 147 | } 148 | 149 | // Stopped; Don't trigger crash 150 | if (this.server.status !== Status.ON && _.isArray(_.get(this.config, 'startup.userInteraction'))) { 151 | Async.each(_.get(this.config, 'startup.userInteraction'), string => { 152 | if (_.includes(data, string)) { 153 | this.server.log.info('Server detected as requiring user interaction, stopping now.'); 154 | this.server.stop(err => { 155 | if (err) this.server.log.warn(err); 156 | }); 157 | } 158 | }); 159 | } 160 | }, 161 | ]); 162 | } 163 | }; 164 | --------------------------------------------------------------------------------