diff --git a/.save b/.save new file mode 100644 index 0000000..d36709e --- /dev/null +++ b/.save @@ -0,0 +1 @@ +{"isFirstRun":false,"allowedApiHosts":["localhost"],"loginDetails":{"username":"root","password":"13377"}} \ No newline at end of file diff --git a/cfg.js.example b/cfg.js.example index d90ab73..15ddac0 100644 --- a/cfg.js.example +++ b/cfg.js.example @@ -6,8 +6,8 @@ export default { // show greeting GREET: true, - // emit api calls - ENABLE_API: false, + // allow api calls + ENABLE_API: true, // Server settings MAX_CONNECTIONS: 64, diff --git a/package.json b/package.json index d089a87..15cf40b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.13.1", "babel-preset-stage-0": "^6.5.0", + "url": "^0.11.0", + "prompt": "^1.0.0", "fs-extra": "^0.30.0", "mysql": "^2.11.1", "jwt-decode": "^2.1.0", diff --git a/src/api.js b/src/api.js index 121f8a4..47640a9 100644 --- a/src/api.js +++ b/src/api.js @@ -1,44 +1,138 @@ +import fs from "fs"; +import url from "url"; +import prompt from "prompt"; + import CFG from "../cfg"; -export function ready(e) { - if (e) console.log(e); - else console.log("Ready!"); +prompt.start({ + message: " ", + delimiter: " " +}); + +export function processApiCall(req, res, route) { + + let save = JSON.parse(fs.readFileSync(".save", "utf8")); + let allowedHosts = save.allowedApiHosts; + + let hoster = url.parse(req.headers.referer).host; + + if (!(allowedHosts.indexOf(hoster) > -1)) { + this.grantApiAccess(req, res, route); + return void 0; + } + + let raw = req.body.toString(); + let json = null; + + try { + json = JSON.parse(raw); + } catch (e) { + this.print(e, 31); + this.answerApiCall(res, ""); + return void 0; + } + + if (this.isApiCall(json)) { + json.host = hoster; + if (json.action === "login") { + let result = this["api_login"](json); + this.answerApiCall(res, JSON.stringify(result)); + } + else { + if (this.apiClients[hoster]) { + let result = this["api_" + json.action](json); + this.answerApiCall(res, JSON.stringify(result)); + } + else { + this.print(`${hoster} isnt logged in! Kicking..`, 31); + } + } + } + else { + if (json.action === "init") { + this.answerApiCall(res, JSON.stringify({ success: true })); + } + } + } -/** - * @param {String} msg - * @param {Number} color - * @param {Boolean} nl - */ -export function print(msg, color, newline) { - color = Number.isInteger(color) ? color : CFG.DEFAULT_CONSOLE_COLOR; - process.stdout.write(`[Console] \x1b[${color};1m${msg}\x1b[0m${newline === void 0 ? "\n" : ""}`); +export function grantApiAccess(req, res, route) { + + let save = JSON.parse(fs.readFileSync(".save", "utf8")); + + let hoster = url.parse(req.headers.referer).host; + + let msg = `[Console] \x1b[33mGrant API access to ${hoster}?\x1b[0m`; + + prompt.get([{ name: "grant", required: true, description: msg }], (e, result) => { + if (result.grant === "y" || result.grant === "yes") { + save.allowedApiHosts.push(hoster); + fs.writeFileSync(".save", JSON.stringify(save), "utf8"); + this.print(`Successfully added ${hoster} to allowed API hosts!`); + this.processApiCall(req, res, route); + } + else { + this.print(`Denied API access for ${hoster}`, 31); + this.answerApiCall(res, ""); + } + }); + } -/** - * @param {Player} player - */ -export function registerPlayer(player) { - console.log(player.username + " registered!"); +export function answerApiCall(res, data) { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); + res.setHeader("Access-Control-Allow-Headers", "X-Requested-With,content-type"); + res.setHeader("Access-Control-Allow-Credentials", true); + res.end(data); } -/** - * @param {Player} player - */ -export function loginPlayer(player) { - console.log(player.username + " logged in!"); +export function api_login(data) { + + if (typeof data !== "object") return void 0; + + let save = JSON.parse(fs.readFileSync(".save", "utf8")); + + let success = false; + + let username = save.loginDetails.username; + let password = save.loginDetails.password; + + if ( + username === data.username && + password === data.password + ) { + success = true; + if (!this.apiClients[data.host]) { + this.print(`API access for ${data.host} granted!`); + } + this.print(`${data.host} logged in!`, 36); + this.apiClients[data.host] = { + timestamp: +new Date() + }; + } + return ({ + success: success + }); } -/** - * @param {Player} player - */ -export function killPlayer(player) { - console.log(player.username + " killed!"); +export function api_getConnectedPlayers() { + return ({ + connected_players: this.world.players.length + }); } -/** - * @param {Player} player - */ -export function updatePlayerAvatar(player) { - console.log("Updated avatar of " + player.username + "!"); +export function api_getServerVersion() { + return ({ + version: CFG.VERSION + }); +} + +export function api_spawnPkmnToPlayer(data) { + let name = String(data.player); + let pkmn = String(data.pkmn).toUpperCase(); + this.print(`Spawned 1x ${pkmn}'s to ${name}!`); + return ({ + success: true + }); } \ No newline at end of file diff --git a/src/db/create.js b/src/db/create.js index dd2ae45..a3fa4c9 100644 --- a/src/db/create.js +++ b/src/db/create.js @@ -3,7 +3,7 @@ import CFG from "../../cfg"; export function createTableIfNotExists(name) { return new Promise((resolve) => { - this.db.instance.query(`SHOW TABLES LIKE '${name}';`, (e, rows, fields) => { + this.db.query(`SHOW TABLES LIKE '${name}';`, (e, rows, fields) => { if (e) console.log(e); else { // exists @@ -39,7 +39,7 @@ export function createTable(name) { `; return new Promise((resolve) => { - this.db.instance.query(query, (e, rows) => { + this.db.query(query, (e, rows) => { if (e) console.log(e); else resolve(); }); diff --git a/src/db/get.js b/src/db/get.js index 3d9b9af..93fbfd4 100644 --- a/src/db/get.js +++ b/src/db/get.js @@ -7,7 +7,7 @@ import CFG from "../../cfg"; */ export function getQueryByColumnFromTable(column, value, table) { return new Promise((resolve) => { - this.db.instance.query(`SELECT * FROM ${table} WHERE ${column}=?`, [value], (e, rows) => { + this.db.query(`SELECT * FROM ${table} WHERE ${column}=?`, [value], (e, rows) => { if (e) console.log(e); if (rows && rows.length) resolve(rows); else resolve(void 0); @@ -22,7 +22,7 @@ export function getQueryByColumnFromTable(column, value, table) { */ export function deleteQueryByColumnFromTable(column, value, table) { return new Promise((resolve) => { - this.db.instance.query(`DELETE FROM ${table} WHERE ${column}=?`, [value], (e, rows) => { + this.db.query(`DELETE FROM ${table} WHERE ${column}=?`, [value], (e, rows) => { if (e) console.log(e); else resolve(void 0); }); diff --git a/src/db/index.js b/src/db/index.js index 2e6f41b..adcc0a5 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -19,7 +19,7 @@ export function setupDatabaseConnection() { this.retry("Retrying again in ", () => this.setupDatabaseConnection().then(resolve), 5); return void 0; } - this.db.instance = connection; + this.db = connection; this.createTableIfNotExists(CFG.MYSQL_USERS_TABLE).then(() => { this.createTableIfNotExists(CFG.MYSQL_OWNED_PKMN_TABLE).then(() => { this.print(`\x1b[36;1mMySQL\x1b[0m\x1b[32;1m connection established\x1b[0m`); @@ -39,7 +39,7 @@ export function setupDatabaseConnection() { * @param {Function} resolve */ export function closeConnection(resolve) { - this.db.instance.end(() => { + this.db.end(() => { resolve(); }); } \ No newline at end of file diff --git a/src/http.js b/src/http.js index 4eede73..ffcc1ac 100644 --- a/src/http.js +++ b/src/http.js @@ -18,9 +18,7 @@ export function createHTTPServer() { req.on("end", () => { let buffer = Buffer.concat(chunks); req.body = buffer; - this.world.getPlayerByRequest(req, res).then((player) => { - this.routeRequest(player, req, res); - }); + this.routeRequest(req, res); }); }); server.listen(CFG.PORT); diff --git a/src/index.js b/src/index.js index 4226ca2..af63d5a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,5 @@ import fs from "fs"; import os from "os"; -import EventEmitter from "events"; import POGOProtos from "pokemongo-protobuf"; import { @@ -30,23 +29,20 @@ const greetMessage = fs.readFileSync(".greet", "utf8"); /** * @class GameServer */ -export default class GameServer extends EventEmitter { +export default class GameServer { /** @constructor */ constructor() { - super(null); - this.STATES = { PAUSE: false, DEBUG: false, CRASH: false }; - this.db = { - instance: null, - collections: {} - }; + this.db = null; + + this.apiClients = {}; this.assets = {}; this.master = null; @@ -61,8 +57,6 @@ export default class GameServer extends EventEmitter { this.timeoutTick = 0; this.passedTicks = 0; - this.initAPI(); - if (CFG.GREET) this.greet(); this.print(`Booting Server v${require("../package.json").version}-dev`, 33); @@ -73,25 +67,25 @@ export default class GameServer extends EventEmitter { } - initAPI() { - if (CFG.ENABLE_API) { - for (let key in _api) { - this.on(key, _api[key]); - }; - } - // make sure we still have our print fn - else { - this.on("print", _api["print"]); - } - } - /** * @param {String} msg * @param {Number} color * @param {Boolean} nl */ - print(msg, color, nl) { - this.emit("print", msg, color, nl); + print(msg, color, newline) { + color = Number.isInteger(color) ? color : CFG.DEFAULT_CONSOLE_COLOR; + process.stdout.write(`[Console] \x1b[${color};1m${msg}\x1b[0m${newline === void 0 ? "\n" : ""}`); + } + + /** + * @param {Object} obj + * @return {Boolean} + */ + isApiCall(call) { + let action = String(call.action); + return ( + "api_" + action in _api + ); } /** @@ -163,6 +157,7 @@ export default class GameServer extends EventEmitter { } +inherit(GameServer, _api); inherit(GameServer, _dump); inherit(GameServer, _http); inherit(GameServer, _setup); diff --git a/src/models/Player/index.js b/src/models/Player/index.js index 7a98157..cd6fe7a 100644 --- a/src/models/Player/index.js +++ b/src/models/Player/index.js @@ -20,14 +20,14 @@ export default class Player extends MapObject { this.response = obj.response; this.authenticated = 0; - +/* this.bag = new Bag(this); this.info = new Info(this); this.pary = new Party(this); this.avatar = new Avatar(this); this.pokedex = new Pokedex(this); this.tutorial = new Tutorial(this); - +*/ } } \ No newline at end of file diff --git a/src/models/World/index.js b/src/models/World/index.js index 017a75a..b1ef05b 100644 --- a/src/models/World/index.js +++ b/src/models/World/index.js @@ -8,7 +8,11 @@ import CFG from "../../../cfg"; export default class World { /** @constructor */ - constructor() { + constructor(instance) { + + this.instance = instance; + + this.db = this.instance.db; this.players = []; @@ -31,16 +35,14 @@ export default class World { let player = null; - return new Promise((resolve) => { - if (this.playerAlreadyConnected(req)) { - player = this.getPlayerByIP(req.headers.host); - resolve(player); - } - else { - this.addPlayer(req, res); - resolve(player); - } - }); + if (this.playerAlreadyConnected(req)) { + player = this.getPlayerByIP(req.headers.host); + return (player); + } + else { + this.addPlayer(req, res); + return (player); + } } diff --git a/src/request.js b/src/request.js index 9702deb..60b70b0 100644 --- a/src/request.js +++ b/src/request.js @@ -6,11 +6,10 @@ import POGOProtos from "pokemongo-protobuf"; import CFG from "../cfg"; /** - * @param {Player} player * @param {Request} req * @param {Response} res */ -export function routeRequest(player, req, res) { +export function routeRequest(req, res) { let parsed = url.parse(req.url).pathname; let route = parsed.split("/"); @@ -19,31 +18,60 @@ export function routeRequest(player, req, res) { switch (route[1]) { case "plfe": case "custom": - if (route[2] === "rpc") this.onRequest(player, req); + this.processRpcRequest(req, res, route); break; case "model": - // make sure no random dudes can access download - if (!player.authenticated) return void 0; - let name = route[2]; - if (name && name.length > 1) { - let folder = player.isAndroid ? "android/" : "ios/"; - fs.readFile("data/" + folder + name, (error, data) => { - if (error) { - this.print(`Error file resolving model ${name}:` + error, 31); - return void 0; - } - this.print(`Sent ${name} to ${player.email}`, 36); - res.end(data); - }); + this.processModelRequest(req, res, route); + break; + case "api": + if (!CFG.ENABLE_API) return void 0; + if (req.method === "POST") { + this.processApiCall(req, res, route); } break; default: - console.log(`Unknown request url: https://${req.headers.host}${req.url}`); + console.log(`Unknown request url: https://${host}${req.url}`); break; }; } +/** + * @param {Request} req + * @param {Response} res + * @param {Array} route + */ +export function processRpcRequest(req, res, route) { + let player = this.world.getPlayerByRequest(req, res); + if (route[2] === "rpc") this.onRequest(player, req); +} + +/** + * @param {Request} req + * @param {Response} res + * @param {Array} route + */ +export function processModelRequest(req, res, route) { + + let player = this.world.getPlayerByRequest(req, res); + + // make sure no random dudes can access download + if (!player.authenticated) return void 0; + let name = route[2]; + if (name && name.length > 1) { + let folder = player.isAndroid ? "android/" : "ios/"; + fs.readFile("data/" + folder + name, (error, data) => { + if (error) { + this.print(`Error file resolving model ${name}:` + error, 31); + return void 0; + } + this.print(`Sent ${name} to ${player.email}`, 36); + res.end(data); + }); + } + +} + /** * @param {Player} player * @param {Request} req diff --git a/src/setup.js b/src/setup.js index 498978a..e14c153 100644 --- a/src/setup.js +++ b/src/setup.js @@ -16,12 +16,14 @@ import { export function setup() { - let isFirstRun = !this.fileExists(CFG.DUMP_ASSET_PATH); + let save = JSON.parse(fs.readFileSync(".save", "utf8")); - if (isFirstRun) { - this.print("Required assets are missing! Preparing dump session..", 31); + if (save.isFirstRun) { + this.print("Required assets are missing! Preparing dump session..", 33); setTimeout(() => { this.onFirstRun(() => { + save.isFirstRun = false; + fs.writeFileSync(".save", JSON.stringify(save), "utf8"); this.setup(); }); }, 1e3); @@ -46,13 +48,11 @@ export function setup() { setTimeout(this::this.cycle, 1); let localIPv4 = this.getLocalIPv4(); this.print(`Server listening at ${localIPv4}:${CFG.PORT}`, 33); - this.emit("ready", void 0); }); }).catch((e) => { //fse.removeSync(CFG.DUMP_ASSET_PATH); this.print("Error: " + e + " was not found!", 31); - this.emit("ready", e); }); }