cloud-core.js
const Events = require('events');
const { spawn, exec } = require('child_process');
const express = require('express');
const ws = require('ws');
const fs = require('fs');
class CloudCore extends Events {
/**
* Create server instance.
*
* @param {array} config Array of options, help on git.
*/
constructor(config) {
//load in Events
super();
//construct config with default values
const defaultConfig = {
core: {
path: config.core.path || './',
prefix: config.core.prefix || '',
jar: config.core.jar || 'server.jar',
args: config.core.args || ['-Xmx2G', '-Xms1G'],
authorization: config.core.authorization || "hackme",
backups: {
enabled: config.core.backups.enabled || false,
directory: config.core.backups.directory || "./backup/",
time: config.core.backups.time || "weekly" //can be "weekly", "monthly"
},
port: config.core.port || 25565
},
remote: {
bind: config.remote.bind || "0.0.0.0",
port: config.remote.port || 35565
}
};
config = defaultConfig;
this.config = config; //config from construct
this.wsServer = null; //websocket
this.httpServer = null; //express
this.minecraftServer = null; //java spawn command
process.on('exit', () => {
this.stop()
});
process.on('close', () => {
this.stop()
});
this.wsServer = new ws.Server({ noServer: true });
this.wsServer.on('connection', socket => {
//receive command from client
socket.on('message', (message) => {
try {
message = JSON.parse(message);
} catch (e) {
socket.send(e);
}
let usertext = "";
if (message.user) {
usertext = message.user+" is ";
}
//parse actions
if (message.action == "command") {
this.send(message.command, message.user || undefined);
} else if (message.action == "stop") {
this.log(`[Cloud Core] ${usertext}Stopping the server...`);
this.stop(() => {
this.log(`[Cloud Core] Successfully stopped the server`);
});
} else if (message.action == "clearSessionLock") {
this.clearSessionLock()
} else if (message.action == "kill") {
this.log(`[Cloud Core] ${usertext}Attempting to stop the server...`);
this.stop(() => {
this.log(`[Cloud Core] Killing the server...`);
process.exit();
})
setTimeout(() => {
this.minecraftServer.kill();
process.exit();
}, 8000)
} else if (message.action == "restart") {
this.log(`[Cloud Core] ${usertext}Restarting the server...`);
this.stop(() => {
this.log(`[Cloud Core] Starting the server...`);
this.start();
});
} else if (message.action == "start") {
this.log(`[Cloud Core] ${usertext}Starting the server...`);
this.start();
} else if (message.action == "backup") {
this.backup();
} else if (message.action == "ping") {
socket.send("pong");
}
});
//send console to client
this.on("console", (line) => {
if (line.includes("Done (")) {
this.emit("started");
}
socket.send(line);
})
});
let app = express();
app.use(express.json());
//define api routes
app.use("*", (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*');
if (req.headers['authorization'] == config.core.authorization) {
next();
} else {
res.type("json");
res.send(JSON.stringify(
{
"error": {
"code": 502,
"reason": "Unauthorized. This usually means you have the wrong authentication code in your header."
}
}
));
}
})
app.post("*", (req, res) => {
//post to minecraft
if (req.body.command) {
if (req.body.command == "restart") {
this.log(`[Cloud Core] Restarting the server...`);
this.stop(() => {
this.log(`[Cloud Core] Starting the server...`);
this.start();
});
} else if (req.body.command == "backup") {
this.backup();
} else if (req.body.command == "start") {
this.start();
} else {
this.send(req.body.command, req.body.user || undefined);
}
res.end();
} else {
res.end();
}
})
app.get("*/usage", (req, res) => {
res.type("json");
let config = this.config;
if (this.minecraftServer) {
if (this.moduleAvailable("usage")) {
const usage = require('usage');
usage.lookup(this.minecraftServer.pid, function(err, result) {
if (!err) {
result.config = config;
res.end(JSON.stringify(result));
} else {
res.end(JSON.stringify({"error": err}));
}
});
} else {
res.end(JSON.stringify({"error": "Install the usage module by doing 'npm install usage' inside the same directory the server is in. This is not supported on windows!"}));
}
} else {
res.end(JSON.stringify({"error": "Server not started"}));
}
})
app.get("*", (req, res) => {
const readLastLines = require('read-last-lines');
readLastLines.read('logs/latest.log', 100)
.then((lines) => {
res.end(lines)
});
})
//listen on server
this.httpServer = app.listen(config.remote.port, config.remote.bind || "0.0.0.0", () => {
this.log(`[Cloud Core] Started webserver running on ${config.remote.bind || "0.0.0.0"}:${config.remote.port}`)
});
//upgrade websocket requests
this.httpServer.on('upgrade', (request, socket, head) => {
this.wsServer.handleUpgrade(request, socket, head, socket => {
//make sure auth is correct, specified in the protocol location
if (request.headers['sec-websocket-protocol'] == config.core.authorization) {
this.wsServer.emit('connection', socket, request);
} else {
socket.send("[Cloud Core] Unauthorized. This usually means you have the wrong authentication code in your header.");
socket.close(1000, "Unauthorized");
}
});
});
//auto backups
if (config.core.backups.enabled) {
this.log(`[Cloud Core] Automatic Server Backups enabled. Backups will be taken ${config.core.backups.time}.`)
setTimeout(() => {
let d = new Date();
if (config.core.backups.time == "weekly") {
if (d.getMinutes() == 1 && d.getHours() == 1 && d.getDay() == 1) {
this.backup();
}
} else if (config.core.backups.time == "monthly") {
if (d.getMinutes() == 1 && d.getHours() == 1 && d.getDate() == 1) {
this.backup();
}
}
}, 60000)
}
//emit that we are ready to go
setTimeout(() => {
this.emit("ready", config);
}, 500)
return this;
}
moduleAvailable(name) {
try {
require.resolve(name);
return true;
} catch(e){}
return false;
}
log(msg) {
if (fs.existsSync("logs/latest.log")) {
fs.appendFileSync("logs/latest.log", `${msg}\n`);
}
this.emit('console', msg);
console.log(msg);
}
/**
* Send command to server
*
* @param {String} command Command to send to server
* @param {String} user Optional. User to assign command to
*/
send(command, user) {
//command event
this.emit("command", command);
//send to minecraft server
this.minecraftServer.stdin.write(`${command}\n`);
//send to websocket
this.log(`> ${command} ${user ? "(Issued by "+user+")" : ""}`);
}
/**
* Starts Minecraft server
*
* @return {ChildProcessWithoutNullStreams} Started Minecraft server child process.
*/
start() {
if (!this.minecraftServer) {
//make args
let args = this.config.core.args.concat('-jar', this.config.core.jar);
args = args.concat('--port', this.config.core.port, 'nogui');
//start server
this.minecraftServer = spawn(this.config.core.prefix+'java', args, {cwd: this.config.core.path});
//minecraft to console
this.minecraftServer.stdout.pipe(process.stdout);
//console to minecraft
process.stdin.pipe(this.minecraftServer.stdin);
process.stderr.on('data', (data) => {
this.log(`ERROR: ${data}`);
});
//minecraft to event
this.minecraftServer.stdout.on('data', (d) => {
d.toString().split('\n').forEach((l) => {
if (l) this.emit('console', l.trim());
});
});
this.minecraftServer.on('exit', () => {
this.minecraftServer.kill();
this.minecraftServer = null;
})
//start event
this.emit("start");
if (!fs.existsSync("eula.txt")) {
fs.writeFileSync("eula.txt", "eula=true");
this.log("[Cloud Core] Automatically agreed to EULA.");
}
} else {
this.log("[Cloud Core] Server already started!")
}
return this.minecraftServer;
}
/**
* Stops server
*
* @param {Function} callback Callback when server is stopped
*/
stop(callback) {
//stopping event
this.emit("stopping");
//stop server
if (this.minecraftServer) {
//write stop command
this.minecraftServer.kill('SIGINT');
this.minecraftServer.on('exit', () => {
//on exit
if (this.minecraftServer) {
this.minecraftServer.kill();
}
this.minecraftServer = null;
if (callback) {
callback();
}
//stop event
this.emit("stop");
});
} else {
if (callback) {
callback();
}
//stop event
this.emit("stop");
}
}
/**
* Backup the whole server.
*/
backup() {
if (!fs.existsSync(this.config.core.backups.directory)) {
fs.mkdirSync(this.config.core.backups.directory);
}
let d = new Date();
let name = d.toString().replace(/ /g, "-").split("-(")[0]+".zip";
this.log(`[Cloud Core] Starting server backup...`);
let backup = exec(`zip -r ${this.config.core.backups.directory}${name} * -x backup/*`, {maxBuffer: 1024 * 999999999}, (error, stdout, stderr) => {
this.log(stdout);
if (stderr) {
this.log(`[Cloud Core] Backup Error: ${stderr}`);
}
if (error) {
this.log(`[Cloud Core] Backup Error: ${error.code}`);
console.log(error.stack);
}
});
backup.on('exit', () => {
this.emit("backup", `${this.config.core.backups.directory}${name}`);
this.log(`[Cloud Core] Made backup (${this.config.core.backups.directory}${name})`);
});
}
clearSessionLock() {
let sessionClear = exec('find -type f -name "session.lock" -delete', (error) => {
if (error) {
this.log(`[Cloud Core] Error: ${error.code}`);
console.log(error.stack);
}
})
sessionClear.on('exit', () => {
this.log(`[Cloud Core] Cleared session lock on all worlds.`);
});
}
}
module.exports = CloudCore;