├── .gitignore ├── LICENSE.md ├── README.md ├── clank ├── client.js ├── crypto │ ├── checksum.js │ ├── keys.json │ ├── rc4.js │ └── rsa.js ├── encryptedpacket.js ├── events │ ├── commandsevent.js │ ├── discordevent.js │ └── httpevent.js ├── logger.js ├── mas │ └── handler.js ├── mls │ └── player.js ├── network.js ├── packet.js ├── packet_ids.json ├── packets.json └── util.js ├── config ├── mas.json.example ├── mls.json.example └── mps.json.example ├── debug.sh ├── launch.sh ├── package.json ├── server.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Other files 2 | tmp/ 3 | config/* 4 | !config/mas.json.example 5 | !config/mls.json.example 6 | !config/mps.json.example 7 | package-lock.json 8 | yarn-debug.log* 9 | yarn-error.log* 10 | node_modules/ 11 | .node_repl_history 12 | 13 | # Logs 14 | logs/ 15 | fatal.txt 16 | *.bak 17 | *.log 18 | npm-debug.log* 19 | 20 | # Runtime 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hashsploit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clank - A Ratchet & Clank 3 Server Emulator 2 | 3 | Built for [UYA Online](https://uyaonline.com/). Join our [Discord](https://discord.gg/mUQzqGu) server for updates. 4 | 5 | ## About 6 | This project is a server emulator for the PlayStation 2 / Playstation 3 7 | game Ratchet & Clank: Up Your Arsenal to replace the original production 8 | servers located at `ratchet3-prod1.pdonline.scea.com` for US and 9 | `randc3-master.online.scee.com` for EU. 10 | 11 | By emulating the SCE-RT Medius server stack (which is normally 12 | divided into 6 servers [not including DNAS and a database]) we are 13 | able to communicate with the PS2/PS3 clients. This server aims to be 14 | modular and compact, therefore some components of Medius are merged. 15 | 16 | This server emulator is divided into 3 services: 17 | - **The Medius Authentication Server (MAS)** is where players initially login using 18 | an existing profile and get a `session token` and `ip address` that is then 19 | used to login to the Medius Lobby Server. 20 | - **The Medius Lobby Server (MLS)** is where a majority of players reside when they 21 | are not in game, chatting, looking for a game, managing clans, or looking at 22 | stats. 23 | - **The Medius Proxy Server (MPS)** is where players are synchronized in-game before 24 | DME (Distributed Memory Engine) takes place. 25 | 26 | You can read more about these components [here](https://wiki.hashsploit.net/PlayStation_2#Medius). 27 | 28 | ## Features 29 | 30 | Emulator features that are complete will be checked, features that are still in progress or planned are un-checked. 31 | 32 | - [x] Modular design. 33 | - [ ] Emulates Medius Authentication Server (MAS). 34 | - [ ] Emulates Medius Lobby Server (MLS). 35 | - [ ] Emulates Medius Proxy Server (MPS). 36 | - [ ] Configurable player server operators. 37 | - [ ] Send custom server messages to clients. 38 | - [ ] Server operator chat commands. 39 | - [ ] Configurable EULA screen. 40 | - [ ] Configurable Announcements screen. 41 | - [ ] Configurable death messages. 42 | - [ ] UYA Online API integration. 43 | 44 | ### Prerequisites 45 | - curl (7.52+) 46 | - nodejs (10.3+) 47 | - big-integer (1.6.48+), 48 | - chalk (2.3.1+) 49 | - colors (1.1.2+) 50 | - request (2.88.0+) 51 | - sha1 (1.1.1+) 52 | - sync-request (6.0.0+) 53 | - threads (0.11.0+) 54 | 55 | 56 | ## Configuration 57 | 58 | This server can run in 3 emulation modes, **MAS**, **MLS** and **MPS**. You can have multiple configuration files each with one of the different emulation modes. 59 | 60 | See the table below for a reference of the configuration JSON: 61 | 62 | | Name | Type | Description | 63 | |---------------------|---------|-------------------------------------------------------------------------------------------------------| 64 | | mode | string | One of the following: `mas`, `mls`, or `mps`. | 65 | | address | string | Address the server should bind to. This can be set to an empty string for any. | 66 | | port | integer | Port that the server should listen on. | 67 | | capacity | integer | Maximum number of players this server can handle. | 68 | | log_level | string | Controls logging verbosity. Either: `debug`, `info`, `warn` or `error`. | 69 | | api | object | Details to hook into UYA Online's API. (this is equivalent to the MUM). | 70 | | whitelist | object | Whitelisted player usernames for testing. All other players will be denied login if this is enabled. | 71 | | discord_webhooks | object | JSON objects of WebHookable events that can be used to broadcast to Discord. | 72 | | client_timeout | integer | Time in milliseconds before a client is automatically disconnected without a heartbeat. | 73 | | max_login_attempts | integer | **MAS Only:** Number of invalid login attempts made by a single player before being soft-banned. | 74 | | mls_ip_address | string | **MAS Only:** Set this to the MLS's address. If it is null it will be auto-obtained. | 75 | | operators | array | **MLS Only:** An array of usernames of players that are server operators. | 76 | | command_prefix | string | **MLS Only:** A string prefix used to determine what in chat should be evaluated as a system command. | 77 | | eula | array | **MLS Only:** An array of strings to send to the client as the EULA message. | 78 | | announcements | array | **MLS Only:** An array of strings to send to the client on the Announcements page. | 79 | | death_messages | array | **MPS Only:** An array of death messages to be selected at random. | 80 | | death_messages | array | **MPS Only:** An array of death messages to be selected at random. | 81 | 82 | 83 | 84 | ### MAS (Medius Authentication Server) 85 | 86 | The server emulator will act as an authentication server for handling user logins. 87 | 88 | ### MLS (Medius Lobby Server) 89 | 90 | The server emulator will act as a lobby server for handling out-of game events. 91 | 92 | ### MPS (Medius Proxy Server) 93 | 94 | The server emulator will act as a proxy server and manage in-game matches. 95 | 96 | ## Setup 97 | 1. Download or clone the project. `git clone https://github.com/hashsploit/clank`. 98 | 2. Run `npm i` in the directory of the project to install the required packages. 99 | 3. Copy `config/mas.json.example` to `config/mas.json` and configure it. 100 | 4. Run `./launch.sh mas` to start the `mas` server. If you are debugging, you can manually run `nodejs --trace-warnings server.js mas.json`. 101 | -------------------------------------------------------------------------------- /clank/client.js: -------------------------------------------------------------------------------- 1 | let logger = require('./logger.js'); 2 | let network = require('./network.js'); 3 | 4 | function Client(socket) { 5 | this.socket = socket; 6 | this.username; 7 | this.clientState = 0; // Connection stage (before logged_in is set to true) 8 | this.operator = false; 9 | 10 | this.start = function() { 11 | 12 | } 13 | 14 | this.send = function(data) { 15 | network.sendData(this, data); 16 | } 17 | 18 | 19 | } 20 | 21 | module.exports = Client; 22 | -------------------------------------------------------------------------------- /clank/crypto/checksum.js: -------------------------------------------------------------------------------- 1 | let logger = require('../logger.js'); 2 | let sha1 = require('sha1'); 3 | 4 | function Checksum(input, packetId) { 5 | 6 | // Compute sha1 hash 7 | let result = hexToBytes(sha1(input)); 8 | 9 | // Inject context inter highest 3 bits 10 | result[3] = Number((result[3] & 0x1F) | ((packetId & 7) << 5)); 11 | return result.slice(0, 4); 12 | } 13 | 14 | module.exports = Checksum; 15 | -------------------------------------------------------------------------------- /clank/crypto/keys.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "GLOBAL MAS KEY | UYA PS2 NTSC | DL HD NPUA", 4 | "n": "10315955513017997681600210131013411322695824559688299373570246338038100843097466504032586443986679280716603540690692615875074465586629501752500179100369237", 5 | "e": "17", 6 | "d": "4854567300243763614870687120476899445974505675147434999327174747312047455575182761195687859800492317495944895566174677168271650454805328075020357360662513" 7 | }, 8 | { 9 | "comment": "CLIENT AUTH KEY | UYA PS2 NTSC", 10 | "n": "10818698864852529169654939372314224042721443840878792146188116838905755590786829011691246645307492409247191122437625676104042595209630473880013285201907563", 11 | "e": "17", 12 | "d": "7000334559610460050953196064438615557055051897039218447533487366350783029332519031581704006912769453840813622797750032241268047460082258615126739128243473" 13 | }, 14 | { 15 | "comment": "CLIENT AUTH KEY | DL PS2 NTSC", 16 | "n": "10050356962645816905344862325421678999857135586090561898962595162395705959736196531277029037839492627511844645395487066542742912877963865473424548324115559", 17 | "e": "17", 18 | "d": "1773592405172791218590269822133237470563023926957157982169869734540418698776940475272404592429013574371415947271566235381419735762279442444574789766216089" 19 | }, 20 | { 21 | "comment": "CLIENT AUTH KEY | RATCHET4-FILTERED.PCAP", 22 | "n": "11804828329923455652899332506012495580942301944697164474911241726445828057459823517236647240911538221817896923261517842239825458535395207572306229449420163", 23 | "e": "17", 24 | "d": "5555213331728685013129097649888233214561083268092783282311172577150977909392755847665823182266626656776361098924782725614261160292065993687968210070306225" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /clank/crypto/rc4.js: -------------------------------------------------------------------------------- 1 | let logger = require('../logger.js'); 2 | let sha1 = require('sha1'); 3 | 4 | function RC4(key, context) { 5 | 6 | 7 | } 8 | 9 | module.exports = RC4; 10 | -------------------------------------------------------------------------------- /clank/crypto/rsa.js: -------------------------------------------------------------------------------- 1 | let logger = require("./logger.js"); 2 | let bigInt = require("big-integer"); 3 | 4 | class RSA { 5 | 6 | constructor(n, e, d) { 7 | this.n = bigInt(n); 8 | this.e = bigInt(e); 9 | this.d = bigInt(d); 10 | } 11 | 12 | 13 | } 14 | 15 | module.exports = RSA; 16 | -------------------------------------------------------------------------------- /clank/encryptedpacket.js: -------------------------------------------------------------------------------- 1 | let logger = require("./logger.js"); 2 | var network = require('./network.js'); 3 | 4 | let packetId; 5 | let packetLength; 6 | let packetChecksum; 7 | let packetPayload; 8 | 9 | class EncryptedPacket { 10 | 11 | constructor(packetId, packetLength, packetChecksum, packetPayload) { 12 | this.packetId = packetId; 13 | this.packetLength = packetLength; 14 | this.packetChecksum = packetChecksum; 15 | this.packetPayload = packetPayload; 16 | } 17 | 18 | decrypt() { 19 | 20 | } 21 | 22 | 23 | } 24 | 25 | module.exports = EncryptedPacket; 26 | -------------------------------------------------------------------------------- /clank/events/commandsevent.js: -------------------------------------------------------------------------------- 1 | var logger = require("../logger.js"); 2 | var EventEmitter = require("events"); 3 | 4 | /* Commands Service */ 5 | let prefix = global.config.server.command_prefix; 6 | var commands = { 7 | "help": {operator: false, function: "getHelp"}, 8 | "ping": {operator: false, function: "getPing"}, 9 | "server": {operator: false, function: "getServer"}, 10 | "kick": {operator: true, function: "handleKick"}, 11 | "info": {operator: true, function: "getPlayerInfo"}, 12 | "mute": {operator: true, function: "handleMute"}, 13 | "stop": {operator: true, function: "handleStop"}, 14 | }; 15 | 16 | let CommandsEvent = new EventEmitter(); 17 | let CommandsModule = new CommandModules(); 18 | 19 | function setupCommandModules() { 20 | 21 | for (let key in commands) { 22 | let operator = command[key].operator; 23 | let cmdFunction = commands[key].function; 24 | 25 | CommandsEvent.on(cmdFunction, (player, message) => { 26 | 27 | // Remove prefix from message 28 | message = message.slice(prefix.length); 29 | let commandArray = message.split(' '); 30 | let commandHandler = commandArray[0]; 31 | let arguments = commandArray.splice(1); 32 | 33 | // Check permission 34 | if (operator && !(player.name in global.config.operators)) { 35 | logger.log("info", "Player {0} ({1}) attempted to execute an operator only command: {2}".format(player.name, player.id, commandHandler)); 36 | return; 37 | } 38 | 39 | logger.log("info", "Player {0} ({1}) issued server command: {2}".format(player.name, player.id, message)); 40 | 41 | CommandsModule[commands[commandHandler]](player, arguments); 42 | }); 43 | } 44 | } 45 | 46 | function isValidCommand(message) { 47 | let prefix = global.config.server.command_prefix; 48 | var index = prefix.indexOf(message[0]); 49 | 50 | if (index == -1) { 51 | return false; 52 | } 53 | 54 | message = message.split(prefix)[1]; 55 | 56 | var commandArray = message.split(' '); 57 | var commandHandler = commandArray[0]; 58 | var arguments = commandArray.splice(1); 59 | 60 | if (commands[commandHandler] == undefined) { 61 | return false; 62 | } 63 | 64 | return commands[commandHandler]; 65 | } 66 | 67 | function CommandModules() { 68 | 69 | // help: show commands 70 | this.getHelp = function(penguin) { 71 | let prefix = global.config.server.command_prefix; 72 | 73 | penguin.send('mm', penguin.room.internal_id, "Moderator Commands:", -1); 74 | penguin.send('mm', penguin.room.internal_id, "ping, server, list, find, info, tp, kick, hide, freeze, switch", -1); 75 | 76 | if (penguin.permission >= 3) { 77 | penguin.send('mm', penguin.room.internal_id, "Administrator Commands:", -1); 78 | penguin.send('mm', penguin.room.internal_id, "room, tphere, mail, coins, item, reboot, bc, stop", -1); 79 | } 80 | 81 | return; 82 | } 83 | 84 | // ping: count # of users in the server total 85 | this.getPing = function(penguin, arguments) { 86 | let prefix = global.config.server.command_prefix; 87 | const USAGE_MSG = "Usage: {0}ping [message]".format(prefix); 88 | 89 | if (arguments) { 90 | if (arguments.length >= 1) { 91 | let msg = arguments.join(' '); 92 | penguin.send('mm', penguin.room.internal_id, "Pong: {0}".format(msg), -1); 93 | penguin.send('e', -1, global.error.SERVER_MESSAGE.id, "\nPong: {0}".format(msg)); 94 | return; 95 | } 96 | } 97 | 98 | penguin.send('mm', penguin.room.internal_id, 'Pong!', -1); 99 | return; 100 | } 101 | 102 | // server: get server stats 103 | this.getServer = function(penguin) { 104 | let totalPlayers = Object.keys(penguinsById).length; 105 | let totalMembers = 0; 106 | let totalHelpers = 0; 107 | let totalModerators = 0; 108 | let totalAdministrators = 0; 109 | 110 | penguin.send('mm', penguin.room.internal_id, "Server: {0} ({1} v{2}) [{3}/{4}]".format(global.config.server.name, global.name, global.version, totalPlayers, global.config.server.capacity), -1); 111 | 112 | for (var otherPenguinId in Object.keys(penguinsById)) { 113 | let otherPenguin = penguins[otherPenguinId]; 114 | 115 | if (otherPenguin.permission == 3) { 116 | totalAdministrators++; 117 | } else if (otherPenguin.permission == 2) { 118 | totalModerators++; 119 | } else if (otherPenguin.permission == 1) { 120 | totalHelpers++; 121 | } else if (otherPenguin.member) { 122 | totalMembers++; 123 | } 124 | } 125 | 126 | var msg = "You're connected to " + global.config.server.name + "\n"; 127 | 128 | msg += "Server ID: {0} Emulator: {1} v{2} Platform: {3}\n" 129 | .format(global.config.server.id, global.name, global.version, process.platform); 130 | msg += "Players: " + totalPlayers + "/" + global.config.server.capacity + 131 | " (Helpers: " + totalHelpers + ") (Mods: " + totalModerators + ") (Admins: " + totalAdministrators + ")" + "\n"; 132 | msg += "Access: " + 133 | (!global.config.server.moderator ? ("Public") : 134 | ("Staff")) + 135 | " Rooms: " + Object.keys(global.rooms).length + 136 | " Igloos: " + Object.keys(global.igloos).length + 137 | " Items: " + Object.keys(global.items).length + "\n"; 138 | msg += "Furniture: " + Object.keys(global.furniture).length + 139 | " CJ Cards: " + Object.keys(global.cards).length + 140 | " EPF Items: " + Object.keys(global.epfItems).length + 141 | " Pins: " + Object.keys(global.pins).length + ""; 142 | 143 | penguin.send('e', -1, global.error.SERVER_MESSAGE.id, msg); 144 | 145 | return; 146 | } 147 | 148 | // info: get information on a player 149 | this.getPlayerInfo = function(penguin, arguments) { 150 | let prefix = global.config.server.command_prefix; 151 | var playerName = arguments[0]; 152 | 153 | if (playerName == undefined) { 154 | penguin.send('mm', penguin.room.internal_id, "Usage: {0}info ".format(prefix), -1); 155 | return; 156 | } 157 | 158 | for (var otherPenguinId in Object.keys(penguinsById)) { 159 | let otherPenguin = penguins[otherPenguinId]; 160 | if (otherPenguin.name().toUpperCase() == playerName.toUpperCase()) { 161 | 162 | var permissionLevels = ["Player", "Helper", "Moderator", "Administrator"]; 163 | 164 | var json = { 165 | "USN/ID": otherPenguin.name() + " (#" + otherPenguin.id + ")", 166 | "Room": otherPenguin.room.name + " (#" + otherPenguin.room.external_id + ")", 167 | "Member": otherPenguin.member ? "true" : "false", 168 | "Muted": otherPenguin.muted ? "true" : "false", 169 | "USN Approved": otherPenguin.approved ? "true" : "false", 170 | "Permission": otherPenguin.permission + " (" + permissionLevels[otherPenguin.permission] + ")", 171 | "EPF Agent": otherPenguin.epf ? "true" : "false", 172 | "Items": otherPenguin.inventory.length, 173 | "Chat": otherPenguin.encryptedChat ? "encrypted" : "unencrypted", 174 | "Coins": otherPenguin.coins, 175 | "Buddies": otherPenguin.buddies.length, 176 | "Stamps": otherPenguin.stamps.length, 177 | "Connection": otherPenguin.isWS ? "WebSocket" : "Socket" 178 | }; 179 | 180 | var chatMessage = "Player " + otherPenguin.name() + " has id #" + otherPenguin.id + "."; 181 | 182 | sendFancyMessage(penguin, json, chatMessage); 183 | 184 | penguin.send('mm', penguin.room.internal_id, "Player '" + otherPenguin.name() + "' in room " + otherPenguin.room.name + " (#" + otherPenguin.room.external_id + ").", -1); 185 | if (otherPenguin.room.is_game) { 186 | penguin.send('mm', penguin.room.internal_id, "Player '" + otherPenguin.name() + "' is playing a game.", -1); 187 | } else { 188 | penguin.send('mm', penguin.room.internal_id, "Player '" + otherPenguin.name() + "' is at (X=" + otherPenguin.x + ", Y=" + otherPenguin.y + ").", -1); 189 | } 190 | 191 | return; 192 | } 193 | } 194 | 195 | penguin.send('mm', penguin.room.internal_id, "Player '" + playerName + "' is offline or on another server.", -1); 196 | penguin.send('e', -1, global.error.SERVER_MESSAGE.id, "\nPlayer '" + playerName + "' is offline or on another server."); 197 | } 198 | 199 | // kick: kick a player 200 | this.handleKick = function(penguin, arguments) { 201 | let prefix = global.config.server.command_prefix; 202 | var playerName = arguments[0]; 203 | 204 | if (playerName == undefined) { 205 | penguin.send('mm', penguin.room.internal_id, "Usage: {0}kick ".format(prefix), -1); 206 | return; 207 | } 208 | 209 | for (var otherPenguinId in Object.keys(penguinsById)) { 210 | let otherPenguin = penguins[otherPenguinId]; 211 | if (otherPenguin.name().trim().toUpperCase() == playerName.trim().toUpperCase()) { 212 | // TODO: Add reason? 213 | kickPlayer(otherPenguin); 214 | penguin.send('mm', penguin.room.internal_id, "Kicked '" + otherPenguin.name() + "'" + "' (#" + otherPenguin.id + ").", -1); 215 | penguin.send('e', -1, global.error.SERVER_MESSAGE.id, "\nKicked '" + otherPenguin.name() + "' (#" + otherPenguin.id + ")"); 216 | return; 217 | } 218 | } 219 | 220 | penguin.send('mm', penguin.room.internal_id, "Player '" + playerName + "' is offline or on another server.", -1); 221 | penguin.send('e', -1, global.error.SERVER_MESSAGE.id, "\nPlayer '" + playerName + "' is offline or on another server."); 222 | } 223 | 224 | // mute: mute a player 225 | this.handleMute = function(penguin, arguments) { 226 | let prefix = global.config.server.command_prefix; 227 | var playerName = arguments[0]; 228 | 229 | if (playerName == undefined) { 230 | penguin.send('mm', penguin.room.internal_id, "Usage: {0}mute ".format(prefix), -1); 231 | return; 232 | } 233 | 234 | for (var otherPenguinId in Object.keys(penguinsById)) { 235 | let otherPenguin = penguins[otherPenguinId]; 236 | if (otherPenguin.name().trim().toUpperCase() == playerName.trim().toUpperCase()) { 237 | otherPenguin.muted = !otherPenguin.muted; 238 | logger.log("info", "Moderator {0} ({1}) {2} player {3}".format(penguin.name(), penguin.id, (penguinsById[playerId].muted ? "muted" : "un-muted"), playerId)); 239 | penguin.send('mm', penguin.room.internal_id, "Player '{0}' (#{1}) is now {2}.".format(penguinsById[playerId].name(), playerId, (penguinsById[playerId].muted ? "muted" : "un-muted")), -1); 240 | penguin.send('e', -1, global.error.SERVER_MESSAGE.id, "Player '" + penguinsById[playerId].name() + "' (#{0}) is now {1}.".format(playerId, (penguinsById[playerId].muted ? "muted" : "un-muted"))); 241 | return; 242 | } 243 | 244 | } 245 | 246 | penguin.send('mm', penguin.room.internal_id, "Player '{0}' is offline or on another server.".format(playerName), -1); 247 | penguin.send('e', -1, global.error.SERVER_MESSAGE.id, "\nPlayer '{0}' is offline or on another server.".format(playerName)); 248 | } 249 | 250 | 251 | // stop: shutdown the server 252 | this.handleStop = function(penguin, arguments) { 253 | if (penguin.permission < 3) { 254 | return; 255 | } 256 | 257 | for (let otherPenguinId in Object.keys(penguinsById)) { 258 | let otherPenguin = penguins[otherPenguinId]; 259 | if (otherPenguin !== undefined) { 260 | otherPenguin.send('e', -1, global.error.SOCKET_LOST_CONNECTION.id); 261 | network.removePenguin(otherPenguin); 262 | } 263 | } 264 | 265 | setTimeout(function() { 266 | global.stopServer(); 267 | }, 2000); 268 | } 269 | 270 | } 271 | 272 | 273 | module.exports.commands = commands; 274 | module.exports.CommandsEvent = CommandsEvent; 275 | module.exports.setupCommandModules = setupCommandModules; 276 | module.exports.isValidCommand = isValidCommand; 277 | -------------------------------------------------------------------------------- /clank/events/discordevent.js: -------------------------------------------------------------------------------- 1 | var logger = require('../logger.js'); 2 | var EventEmitter = require('events'); 3 | 4 | require('./httpevent.js'); 5 | 6 | /* Discord Webhook Service */ 7 | 8 | DiscordEvent = new EventEmitter(); 9 | 10 | // Load webhooks from discord_webhooks in config.json 11 | for (let name in global.config.discord_webhooks) { 12 | let url = global.config.discord_webhooks[name]; 13 | 14 | if (!url) { 15 | continue; 16 | } 17 | 18 | DiscordEvent.on(name, (array) => { 19 | let message = []; 20 | let fields = []; 21 | let thumbnail = null; 22 | 23 | for (var key in array) { 24 | 25 | // Meta-options 26 | if (key.startsWith("_")) { 27 | var k = key.substr(1); 28 | var v = array[key]; 29 | fields.push({"name": k, "value": v, "inline": true}); 30 | continue; 31 | } 32 | if (key === "Icon") { 33 | var statuses = { 34 | "green": "https://uyaonline.com/assets/img/green.png", 35 | "yellow": "https://uyaonline.com/assets/img/yellow.png", 36 | "red": "https://uyaonline.com/assets/img/red.png", 37 | "hovership": "https://uyaonline.com/assets/img/hovership_scaled.png", 38 | "turboslider": "https://uyaonline.com/assets/img/turboslider_scaled.png", 39 | "hovership": "https://uyaonline.com/assets/img/hovership_scaled.png", 40 | 41 | "map_bakisi_isles": "https://uyaonline.com/assets/img/maps/bakisi_isles_map.png", 42 | "map_blackwater_city": "https://uyaonline.com/assets/img/maps/blackwater_city_map.png", 43 | "map_hoven_gorge": "https://uyaonline.com/assets/img/maps/hoven_gorge_map.png", 44 | "map_korgon_outpost": "https://uyaonline.com/assets/img/maps/korgon_outpost_map.png", 45 | "map_metropolis": "https://uyaonline.com/assets/img/maps/metropolis_map.png", 46 | "map_outpost_x12": "https://uyaonline.com/assets/img/maps/outpost_x12_map.png" 47 | }; 48 | var v = array[key]; 49 | for (var k in statuses) { 50 | if (k === v) { 51 | thumbnail = statuses[k]; 52 | } 53 | } 54 | continue; 55 | } 56 | 57 | var value = array[key]; 58 | var string = "**" + key + ":** " + value + ""; 59 | message.push(string); 60 | } 61 | 62 | var dataToSend = { 63 | "embeds": [ 64 | { 65 | "author": { 66 | "name": "{0}".format(global.serverModes[global.config.mode]), 67 | "url": null, 68 | "icon_url": "https://uyaonline.com/favicon.png" 69 | }, 70 | "title": "**Event:** " + name, 71 | "description": message.join("\n"), 72 | "color": 16750848, 73 | "thumbnail": { 74 | "url": thumbnail 75 | }, 76 | "footer": { 77 | "text": global.name.capitalize() + " v" + global.version, 78 | "icon_url": "https://uyaonline.com/favicon.png" 79 | }, 80 | "fields": fields 81 | } 82 | ] 83 | }; 84 | 85 | HTTPEvent.emit('PostRequest', url, dataToSend); 86 | }); 87 | } 88 | 89 | module.exports.DiscordEvent = DiscordEvent; 90 | -------------------------------------------------------------------------------- /clank/events/httpevent.js: -------------------------------------------------------------------------------- 1 | var logger = require('../logger.js'); 2 | var request = require('then-request'); 3 | var EventEmitter = require('events'); 4 | 5 | /* HTTP (GET/POST) Service */ 6 | HTTPEvent = new EventEmitter(); 7 | 8 | // Allow self-signed certificate 9 | if (global.config.log_level == "debug") { 10 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 11 | } 12 | 13 | HTTPEvent.on("POST", (url, body, callback) => { 14 | try { 15 | logger.log("debug", "POST::Request -> {0}".format(url), "cyan"); 16 | request("POST", url, {json: body}).done((res) => { 17 | 18 | if (res.statusCode == 403) { 19 | logger.log("warn", "POST::Response <- Error 403 {0}".format(url)); 20 | return; 21 | } else if (res.statusCode == 404) { 22 | logger.log("warn", "POST::Response <- Error 404 {0}".format(url)); 23 | return; 24 | } 25 | 26 | logger.log("debug", "POST::Response <- {0}".format(url), "cyan"); 27 | if (typeof(callback) == 'function') { 28 | return callback(res); 29 | } 30 | }); 31 | } catch (error) { 32 | logger.log("error", "Error: {0}".format(error)); 33 | } 34 | }); 35 | 36 | HTTPEvent.on("GET", (url, callback) => { 37 | try { 38 | logger.log("debug", "GET::Request -> {0}".format(url), "cyan"); 39 | request("GET", url).done((res) => { 40 | 41 | if (res.statusCode == 403) { 42 | logger.log("warn", "GET::Response <- Error 403 {0}".format(url)); 43 | return; 44 | } else if (res.statusCode == 404) { 45 | logger.log("warn", "GET::Response <- Error 404 {0}".format(url)); 46 | return; 47 | } 48 | 49 | logger.log("debug", "GET::Response <- {0}".format(url), "cyan"); 50 | if (typeof(callback) == 'function') { 51 | return callback(res); 52 | } 53 | }); 54 | } catch (error) { 55 | logger.log("error", "Error: {0}".format(error)); 56 | } 57 | }); 58 | 59 | module.exports.PostEvent = HTTPEvent; 60 | -------------------------------------------------------------------------------- /clank/logger.js: -------------------------------------------------------------------------------- 1 | let chalk = require("chalk"); 2 | let currentLogLevel = 0; 3 | 4 | let level = { 5 | "debug": { 6 | "level": 0, 7 | }, 8 | "info": { 9 | "level": 1, 10 | }, 11 | "warn": { 12 | "level": 2, 13 | "color": "yellow" 14 | }, 15 | "error": { 16 | "level": 3, 17 | "color": "red" 18 | } 19 | }; 20 | 21 | function getDateString() { 22 | var date = new Date(); 23 | var hours = date.getHours(); 24 | var minutes = date.getMinutes(); 25 | var seconds = date.getSeconds(); 26 | var millis = date.getMilliseconds(); 27 | var timestamp = ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2) + ":" + ("0" + seconds).slice(-2) + "." + ("00" + millis).slice(-3); 28 | 29 | return timestamp; 30 | } 31 | 32 | function getCallerFile() { 33 | try { 34 | Error.prepareStackTrace = function (err, stack) { 35 | return stack; 36 | }; 37 | 38 | var err = new Error(); 39 | var callerfile; 40 | var currentfile; 41 | 42 | Error.prepareStackTrace = function(err, stack) { 43 | return stack; 44 | }; 45 | 46 | currentfile = err.stack.shift().getFileName(); 47 | 48 | while (err.stack.length) { 49 | callerfile = err.stack.shift().getFileName(); 50 | 51 | if (currentfile !== callerfile) { 52 | return callerfile; 53 | } 54 | } 55 | 56 | } catch (err) {}; 57 | 58 | return undefined; 59 | } 60 | 61 | function setLogLevel(logLevel) { 62 | if (level[logLevel] == null) { 63 | log("error", "Attempted to set the logger level to an invalid value! Acceptable values are: " + Object.keys(level).join(", ")); 64 | return; 65 | } 66 | currentLogLevel = level[logLevel].level; 67 | } 68 | 69 | function log(logLevel, message, color) { 70 | if (logLevel == null || message == null) { 71 | return; 72 | } 73 | 74 | if (level[logLevel] == null) { 75 | log("error", "Attempted to print message \"" + message + "\" with an invalid log level!"); 76 | return; 77 | } 78 | 79 | // Filter out unessesary 80 | if (level[logLevel].level < currentLogLevel) { 81 | return; 82 | } 83 | 84 | if (color != null) { 85 | message = chalk[color].bold(message); 86 | } 87 | 88 | let outputMessage = "[" + Object.keys(level)[level[logLevel].level].toUpperCase() + "/" + getCallerFile().replace(/^.*[\\\/]/, '') + "] " + message; 89 | 90 | if (level[logLevel].color != null) { 91 | outputMessage = chalk[level[logLevel].color].bold(outputMessage); 92 | } 93 | 94 | console.log(getDateString() + " " + outputMessage.replace(/(?:\r\n|\r|\n)/g, '\\n')); 95 | } 96 | 97 | module.exports.setLogLevel = setLogLevel; 98 | module.exports.log = log; 99 | module.exports.getDateString = getDateString; 100 | -------------------------------------------------------------------------------- /clank/mas/handler.js: -------------------------------------------------------------------------------- 1 | var logger = require('../logger.js'); 2 | var network = require('../network.js'); 3 | 4 | function MASHandler() { 5 | 6 | this.start = function() { 7 | 8 | 9 | } 10 | 11 | 12 | 13 | 14 | } 15 | 16 | module.exports = MASHandler 17 | -------------------------------------------------------------------------------- /clank/mls/player.js: -------------------------------------------------------------------------------- 1 | var logger = require('./logger.js'); 2 | var network = require('./network.js'); 3 | 4 | function Player(client) { 5 | this.client = client; 6 | this.username = client.username; 7 | this.operator = false; 8 | this.muted = false; 9 | this.clan = null; 10 | this.buddies = {}; 11 | this.buddy_requests = {}; 12 | this.game = { 13 | game_id: 0, 14 | game_name: null, 15 | game_password: null, 16 | game_max_slots: 8, 17 | game_weapons: {}, 18 | game_vehicles: true, 19 | game_nodes: true, 20 | game_mode: 0, 21 | game_map: 0, 22 | host: false, 23 | team: 0, 24 | skin: 0, 25 | in_game: false, 26 | in_staging: false, 27 | inventory: {}, 28 | x: 0, 29 | y: 0, 30 | z: 0, 31 | yaw: 0, 32 | pitch: 0 33 | }; 34 | this.in_game = false; 35 | this.in_staging = false; 36 | this.stats = { 37 | rank: 0, 38 | total_kills: 0, 39 | total_games_won: 0, 40 | total_games_lost: 0, 41 | total_games_quit: 0, 42 | total_team_pick_red: 0, 43 | total_team_pick_blue: 0, 44 | total_nodes_captured: 0, 45 | kills_with: { 46 | wrench: 0, 47 | n60_storm: 0, 48 | blitz_gun: 0, 49 | gravity_bomb: 0, 50 | minirocket_tube: 0, 51 | lava_gun: 0, 52 | flux_rifle: 0, 53 | morph_o_ray: 0, 54 | mine_glove: 0 55 | } 56 | }; 57 | 58 | 59 | this.start = function() { 60 | 61 | } 62 | 63 | // Send packet to client 64 | this.send = function(data) { 65 | 66 | // TODO: Process data 67 | 68 | this.client.send(data); 69 | } 70 | 71 | this.addItem = function(itemId, cost = 0, showClient = true) { 72 | if (isNaN(itemId)) { 73 | return; 74 | } 75 | 76 | this.inventory.push(itemId); 77 | 78 | if (cost > 0) { 79 | this.subtractCoins(cost); 80 | } 81 | 82 | this.database.update_column(this.id, 'inventory', this.inventory.join('%')); 83 | 84 | if (showClient) { 85 | this.send('ai', this.room.internal_id, itemId, this.coins); 86 | } 87 | } 88 | 89 | this.removeItem = function(itemId) { 90 | if (isNaN(itemId)) { 91 | return; 92 | } 93 | 94 | var newInventory = []; 95 | 96 | // iterate through all the items and remove the requested itemId 97 | for (var i=0; i 0) { 182 | Promise.each(furnitureList, (furnitureDetails) => { 183 | furnitureDetails = furnitureDetails.split('|'); 184 | 185 | var furnitureId = Number(furnitureDetails[0]); 186 | var quantity = Number(furnitureDetails[1]); 187 | 188 | this.furniture[furnitureId] = quantity; 189 | }); 190 | } 191 | 192 | let ignoreList = row['ignores'].split(','); 193 | 194 | if (ignoreList.length > 0) { 195 | Promise.each(ignoreList, (ignoreDetails) => { 196 | ignoreDetails = ignoreDetails.split(':'); 197 | 198 | var playerId = Number(ignoreDetails[0]); 199 | var playerUsername = String(ignoreDetails[1]); 200 | 201 | this.ignores[playerId] = playerUsername; 202 | this.ignoresById[playerId] = ignoreDetails; 203 | }); 204 | } 205 | 206 | if (this.cards.length > 0) { 207 | Promise.each(this.cards, (cardString) => { 208 | var cardArray = cardString.split('|'); 209 | var cardId = Number(cardArray[1]); 210 | 211 | this.ownedCards.push(cardId); 212 | this.cardsById[cardId] = cardString; 213 | }); 214 | } 215 | 216 | /* remove belt progress */ 217 | let beltItems = [4025, 4026, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 104]; 218 | 219 | if (this.belt == 0 && this.ninja == 0) { 220 | this.ninja = 0; 221 | this.database.update_column(this.id, 'card_jitsu_percentage', this.ninja); 222 | 223 | for (var index in beltItems) { 224 | var beltItem = beltItems[index]; 225 | 226 | var _ind = this.inventory.indexOf(String(beltItem)); 227 | 228 | if(_ind >= 0) { 229 | this.inventory.splice(_ind, 1); 230 | } 231 | } 232 | 233 | this.database.update_column(this.id, 'inventory', this.inventory.join('%')); 234 | } 235 | 236 | return callback(); 237 | }).bind(this)); 238 | } 239 | 240 | this.getPlayerDetails = function() { 241 | return null; 242 | } 243 | } 244 | 245 | module.exports = Player; 246 | -------------------------------------------------------------------------------- /clank/network.js: -------------------------------------------------------------------------------- 1 | var logger = require('./logger.js'); 2 | var EncryptedPacket = require("./encryptedpacket.js"); 3 | var Client = require('./client.js'); 4 | var net = require('net'); 5 | var fs = require('fs'); 6 | var EventEmitter = require('events'); 7 | 8 | global.clients = []; 9 | 10 | function start(address, port) { 11 | let server = net.createServer(); 12 | server.on('connection', onConnection); 13 | server.on('error', (e) => { 14 | logger.log("error", e); 15 | process.exit(1); 16 | }) 17 | 18 | server.listen(port, address); 19 | 20 | logger.log("info", "Listening on {0}:{1}".format(address ? address : "*", port)); 21 | } 22 | 23 | function onConnection(conn) { 24 | logger.log("debug", "Incoming connection > {0}:{1}".format(conn.remoteAddress, conn.remotePort), 'cyan'); 25 | 26 | conn.setTimeout(global.config.client_timeout); 27 | //conn.setEncoding('binary'); 28 | conn.setNoDelay(true); 29 | 30 | if ((clients.length + 1) > global.config.capacity) { 31 | // TODO: Send "server full" packet to client 32 | conn.end(); 33 | conn.destroy(); 34 | logger.log("warn", "The server is full. Players: " + (clients.length + 1)); 35 | return; 36 | } 37 | 38 | let client = new Client(conn); 39 | 40 | client.start(); 41 | clients.push(client); 42 | 43 | client.ip_address = conn.remoteAddress; 44 | client.port = conn.remotePort; 45 | 46 | conn.on('data', (data) => { 47 | return onData(client, data); 48 | }); 49 | conn.on('error', (error) => { 50 | return onError(client, error); 51 | }); 52 | conn.on('timeout', () => { 53 | return onTimeout(client); 54 | }); 55 | conn.once('close', () => { 56 | return onClose(client); 57 | }); 58 | } 59 | 60 | function onTimeout(client) { 61 | logger.log("warn", "Connection timeout: {0}:{1}".format(client.ip_address, client.port)); 62 | disconnectClient(client); 63 | return; 64 | } 65 | 66 | function onData(client, data) { 67 | if (data !== null && data !== "") { 68 | 69 | // Data is bigger than 4096 bytes 70 | if (data.length > 4096) { 71 | disconnectClient(client); 72 | return; 73 | } 74 | 75 | try { 76 | logger.log("debug", "Recieved {0}:{1} > {2}".format(client.ip_address, client.port, prettyHex(data)), 'magenta'); 77 | 78 | let buffer = Buffer.alloc(data.length); 79 | buffer.fill(data, 0, data.length, 'utf8'); 80 | 81 | let array = Int32Array.from(buffer); 82 | 83 | // Handle splitting multiple Packet ID's on packets 84 | let index = 0; 85 | let size = buffer.length; 86 | 87 | while (index < size) { 88 | var len = (array[index + 1] | array[index + 2] << 8); 89 | if (array[index + 0] >= 0x80 && len > 0) { 90 | len += 7; 91 | } 92 | var final = array.slice(index, index+len); 93 | try { 94 | let packetId = final[index + 0]; 95 | let packetLength = [final[index + 2] + final[index + 1]]; 96 | let packetChecksum = [final[index + 3], final[index + 4], final[index + 5], final[index + 6]]; 97 | let packetData = final.slice(7); 98 | //packets.decide(this, client, final); 99 | let packet = new EncryptedPacket(packetId, packetLength, packetChecksum, packetData); 100 | 101 | // TODO: Check if packet is valid, otherwise disconnect client 102 | 103 | // TODO: Decrypt packet 104 | 105 | // TODO: Depending on which mode is enabled process packet on that mode handler 106 | 107 | } catch (error) { 108 | logger.log("error", "Error processing packet: {0}".format(error)); 109 | } 110 | index += len; 111 | } 112 | 113 | } catch (error) { 114 | logger.log("error", error); 115 | disconnectClient(client); 116 | return; 117 | } 118 | } 119 | } 120 | 121 | function onClose(client) { 122 | disconnectClient(client); 123 | } 124 | 125 | function onError(client, error) { 126 | logger.log("error", "Socket error {0}:{1} > {2}".format(client.ip_address, client.port, error)); 127 | disconnectClient(client); 128 | } 129 | 130 | function sendData(client, data) { 131 | if (!client.socket.destroyed) { 132 | client.socket.write(data); 133 | logger.log("debug", "Sent {0}:{1} > {2}".format(data), 'magenta'); 134 | } 135 | } 136 | 137 | function disconnectAll(callback) { 138 | if (clients.length == 0) { 139 | if (typeof(callback) == 'function') { 140 | return callback(); 141 | } 142 | } 143 | 144 | for (var clientId in Object.keys(clients)) { 145 | let client = clients[clientId]; 146 | 147 | if (client !== undefined) { 148 | disconnectClient(client); 149 | } 150 | } 151 | 152 | if (typeof(callback) == 'function') { 153 | return callback(); 154 | } 155 | } 156 | 157 | function disconnectClient(client) { 158 | try { 159 | // If the client exists in the clients array 160 | if (clients.indexOf(client) >= 0) { 161 | 162 | // If this client has passed basic authentication 163 | if (client.clientState > 100) { 164 | 165 | // TODO: check if client is in active game/rooms 166 | // gracefully remove from those lists. 167 | 168 | logger.log("info", "Player {0} ({1}:{2}) disconnected".format(client.username, client.ip_address, client.port), "yellow"); 169 | 170 | HTTPEvent.emit(global.config.api.url + "/player/disconnect", { 171 | username: client.username, 172 | ip_address: client.ip_address, 173 | port: client.port 174 | }); 175 | } 176 | 177 | // Remove from clients array 178 | var index = clients.indexOf(client); 179 | clients.splice(index, 1); 180 | 181 | // Kill socket 182 | if (client.socket !== undefined) { 183 | client.socket.end(); 184 | client.socket.destroy(); 185 | } 186 | 187 | logger.log("debug", "Disconnected > {0}:{1}".format(client.ip_address, client.port), 'cyan'); 188 | } 189 | 190 | } catch(err) { 191 | logger.log("error", "Failed to disconnect client: " + err); 192 | } 193 | } 194 | 195 | module.exports.start = start; 196 | module.exports.sendData = sendData; 197 | module.exports.clients = clients; 198 | module.exports.disconnectClient = disconnectClient; 199 | module.exports.disconnectAll = disconnectAll; 200 | -------------------------------------------------------------------------------- /clank/packet.js: -------------------------------------------------------------------------------- 1 | var logger = require('./logger.js'); 2 | var network = require('./network.js'); 3 | var handler = null; 4 | 5 | function start() { 6 | 7 | } 8 | 9 | function decide(socket, client, data) { 10 | 11 | if (socket == undefined || socket.destroyed || data == null) { 12 | disconnectClient(client); 13 | return; 14 | } 15 | 16 | var parsedPacket = new Parser(data); 17 | 18 | 19 | } 20 | 21 | class Parser { 22 | 23 | constructor(rawData) { 24 | this.splitPacket = [...rawData]; 25 | this.p_id = this.splitPacket[0]; 26 | this.p_length = this.splitPacket[2] + this.splitPacket[1]; 27 | this.p_data = this.splitPacket.slice(1 + 2 + 4, this.splitPacket.length); 28 | this.p_checksum = []; 29 | this.p_checksum.push(this.splitPacket[3]); 30 | this.p_checksum.push(this.splitPacket[4]); 31 | this.p_checksum.push(this.splitPacket[5]); 32 | this.p_checksum.push(this.splitPacket[6]); 33 | 34 | 35 | this.isBadPacket = false; 36 | 37 | logger.log("debug", "Incoming Packet -> id:{0} length:{1} checksum:{2} data:{3}".format("0x" + prettyHex([this.p_id]), this.p_length, prettyHex(this.p_checksum), prettyHex(this.p_data)), "yellow"); 38 | 39 | // TODO: Check if this is a valid packet (verify length and checksum?) 40 | 41 | if (!this.isBadPacket) { 42 | 43 | // Decrypt packet 44 | 45 | 46 | // Process on the current emulation mode 47 | switch (global.config.mode) { 48 | case "mas": 49 | 50 | break; 51 | 52 | case "mls": 53 | 54 | break; 55 | 56 | case "mps": 57 | 58 | break; 59 | } 60 | } 61 | 62 | } 63 | 64 | 65 | } 66 | 67 | module.exports.start = start; 68 | module.exports.decide = decide; 69 | module.exports.Parser = Parser; 70 | -------------------------------------------------------------------------------- /clank/packet_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "CLIENT_CONNECT_TCP": 0x00, 3 | "CLIENT_DISCONNECT": 0x01, 4 | "CLIENT_APP_BROADCAST": 0x02, 5 | "CLIENT_APP_SINGLE": 0x03, 6 | "CLIENT_APP_LIST": 0x04, 7 | "CLIENT_ECHO": 0x05, 8 | "SERVER_CONNECT_REJECT": 0x06, 9 | "SERVER_CONNECT_ACCEPT_TCP": 0x07, 10 | "SERVER_CONNECT_NOTIFY": 0x08, 11 | "SERVER_DISCONNECT_NOTIFY": 0x09, 12 | "SERVER_APP": 0x0a, 13 | "CLIENT_APP_TOSERVER": 0x0b, 14 | "UDP_APP": 0x0c, 15 | "CLIENT_SET_RECV_FLAG": 0x0d, 16 | "CLIENT_SET_AGG_TIME": 0x0e, 17 | "CLIENT_FLUSH_ALL": 0x0f, 18 | "CLIENT_FLUSH_SINGLE": 0x10, 19 | "SERVER_FORCED_DISCONNECT": 0x11, 20 | "CLIENT_CRYPTKEY_PUBLIC": 0x12, 21 | "SERVER_CRYPTKEY_PEER": 0x13, 22 | "SERVER_CRYPTKEY_GAME": 0x14, 23 | "CLIENT_CONNECT_TCP_AUX_UDP": 0x15, 24 | "CLIENT_CONNECT_AUX_UDP": 0x16, 25 | "CLIENT_CONNECT_READY_AUX_UDP": 0x17, 26 | "SERVER_INFO_AUX_UDP": 0x18, 27 | "SERVER_CONNECT_ACCEPT_AUX_UDP": 0x19, 28 | "SERVER_CONNECT_COMPLETE": 0x1a, 29 | "CLIENT_CRYPTKEY_PEER": 0x1b, 30 | "SERVER_SYSTEM_MESSAGE": 0x1c, 31 | "SERVER_CHEAT_QUERY": 0x1d, 32 | "SERVER_MEMORY_POKE": 0x1e, 33 | "SERVER_ECHO": 0x1f, 34 | "CLIENT_DISCONNECT_WITH_REASON": 0x20, 35 | "CLIENT_CONNECT_READY_TCP": 0x21, 36 | "SERVER_CONNECT_REQUIRE": 0x22, 37 | "CLIENT_CONNECT_READY_REQUIRE": 0x23, 38 | "CLIENT_HELLO": 0x24, 39 | "SERVER_HELLO": 0x25, 40 | "SERVER_STARTUP_INFO_NOTIFY": 0x26, 41 | "CLIENT_PEER_QUERY": 0x27, 42 | "SERVER_PEER_QUERY_NOTIFY": 0x28, 43 | "CLIENT_PEER_QUERY_LIST": 0x29, 44 | "SERVER_PEER_QUERY_LIST_NOTIFY": 0x2a, 45 | "CLIENT_WALLCLOCK_QUERY": 0x2b, 46 | "SERVER_WALLCLOCK_QUERY_NOTIFY": 0x2c, 47 | "CLIENT_TIMEBASE_QUERY": 0x2d, 48 | "SERVER_TIMEBASE_QUERY_NOTIFY": 0x2e, 49 | "CLIENT_TOKEN_MESSAGE": 0x2f, 50 | "SERVER_TOKEN_MESSAGE": 0x30, 51 | "CLIENT_SYSTEM_MESSAGE": 0x31, 52 | "CLIENT_APP_BROADCAST_QOS": 0x32, 53 | "CLIENT_APP_SINGLE_QOS": 0x33, 54 | "CLIENT_APP_LIST_QOS": 0x34, 55 | "CLIENT_MAX_MSGLEN": 0x35, 56 | "SERVER_MAX_MSGLEN": 0x36 57 | } 58 | -------------------------------------------------------------------------------- /clank/packets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0x01, 4 | "name": "Graceful Disconnect", 5 | "encrypted": true, 6 | }, 7 | { 8 | "id": 0x92, 9 | "name": "RSA Login Request", 10 | "encrypted": true, 11 | }, 12 | { 13 | "id": 0x93, 14 | "name": "RSA Login Response", 15 | "encrypted": true, 16 | }, 17 | { 18 | "id": 0x9c, 19 | "name": "Server Message", 20 | "encrypted": true, 21 | }, 22 | ] 23 | -------------------------------------------------------------------------------- /clank/util.js: -------------------------------------------------------------------------------- 1 | let logger = require('./logger.js'); 2 | 3 | module.exports = function() { 4 | 5 | global.api = function(endpoint, data) { 6 | if (global.config.api.url) { 7 | HTTPEvent.emit("POST", global.config.api.url + endpoint, data); 8 | } 9 | }; 10 | 11 | global.prettyHex = function(data) { 12 | var output = ""; 13 | for (var i=0; i> 8) & 0xFF); 25 | } 26 | 27 | // Convert a hex string to a byte array 28 | global.hexToBytes = function(hex) { 29 | for (var bytes = [], c = 0; c < hex.length; c += 2) 30 | bytes.push(parseInt(hex.substr(c, 2), 16)); 31 | return bytes; 32 | } 33 | 34 | // Convert a byte array to a hex string 35 | global.bytesToHex = function(bytes) { 36 | for (var hex = [], i = 0; i < bytes.length; i++) { 37 | var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i]; 38 | hex.push((current >>> 4).toString(16)); 39 | hex.push((current & 0xF).toString(16)); 40 | } 41 | return hex.join(""); 42 | } 43 | 44 | if (!String.prototype.format) { 45 | String.prototype.format = function() { 46 | let args = arguments; 47 | 48 | return this.replace(/{(\d+)}/g, function(match, number) { 49 | return typeof args[number] != 'undefined' ? args[number] : match; 50 | }); 51 | } 52 | } 53 | 54 | if (!String.prototype.capitalize) { 55 | String.prototype.capitalize = function() { 56 | return this.charAt(0).toUpperCase() + this.slice(1); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/mas.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "mas", 3 | "address": "", 4 | "port": 10075, 5 | "capacity": 200, 6 | "client_timeout": 45000, 7 | "log_level": "info", 8 | "mls_ip_address": "0.0.0.0", 9 | "api": { 10 | "url": "https://uyaonline.com/api", 11 | "key": "00000000000000000000000000000000" 12 | }, 13 | "max_login_attempts": 20, 14 | "whitelist": { 15 | "enabled": false, 16 | "players": [ 17 | "hashsploit", 18 | "Shanzenos", 19 | "Foas" 20 | ] 21 | }, 22 | "discord_webhooks": { 23 | "start": "", 24 | "shutdown": "", 25 | "login_success": "", 26 | "login_failure": "", 27 | "ban": "" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/mls.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "mls", 3 | "address": "", 4 | "port": 10078, 5 | "capacity": 200, 6 | "client_timeout": 45000, 7 | "log_level": "info", 8 | "api": { 9 | "url": "https://uyaonline.com/api", 10 | "key": "00000000000000000000000000000000" 11 | }, 12 | "command_prefix": "/", 13 | "eula": [ 14 | "None" 15 | ], 16 | "announcements": [ 17 | "Welcome back players" 18 | ], 19 | "operators": [ 20 | "hashsploit", 21 | ], 22 | "whitelist": { 23 | "enabled": false, 24 | "players": [ 25 | "hashsploit", 26 | "Shanzenos", 27 | "Foas" 28 | ] 29 | }, 30 | "discord_webhooks": { 31 | "start": "", 32 | "shutdown": "", 33 | "login_success": "", 34 | "login_failure": "", 35 | "logout": "", 36 | "chat": "", 37 | "create_game": "", 38 | "create_clan": "", 39 | "disban_clan": "", 40 | "ban": "" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/mps.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "mps", 3 | "address": "", 4 | "port": 10078, 5 | "capacity": 200, 6 | "client_timeout": 45000, 7 | "log_level": "info", 8 | "api": { 9 | "url": "https://uyaonline.com/api", 10 | "key": "00000000000000000000000000000000" 11 | }, 12 | "death_messages": [ 13 | "%s killed %s." 14 | ], 15 | "discord_webhooks": { 16 | "start": "", 17 | "shutdown": "", 18 | "game_start": "", 19 | "game_end": "", 20 | "deaths": "", 21 | "ctf_captures": "", 22 | "node_captures": "", 23 | "base_dmg": "" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | if [ "$1" == "" ]; then 6 | echo -e "$(tput bold)$(tput setaf 3)Usage:$(tput sgr0) $0 " 7 | exit 1 8 | fi 9 | 10 | if [ ! -f "./config/$1.json" ]; then 11 | echo -e "$(tput bold)$(tput setaf 1)Error:$(tput sgr0) The configuration file $1.json does not exist." 12 | exit 1 13 | fi 14 | 15 | nodejs --use-strict --trace-warnings server.js $1.json debug 16 | -------------------------------------------------------------------------------- /launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | if [ "$1" == "" ]; then 6 | echo -e "$(tput bold)$(tput setaf 3)Usage:$(tput sgr0) $0 " 7 | exit 1 8 | fi 9 | 10 | if [ ! -f "./config/$1.json" ]; then 11 | echo -e "$(tput bold)$(tput setaf 1)Error:$(tput sgr0) The configuration file $1.json does not exist." 12 | exit 1 13 | fi 14 | 15 | nodejs --use-strict server.js $1.json 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Clank", 3 | "version": "0.1.1", 4 | "description": "A Ratchet & Clank 3 Server Emulator", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "launch.sh" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/hashsploit/clank.git" 13 | }, 14 | "keywords": [ 15 | "clank", 16 | "ratchet", 17 | "ratchet and clank", 18 | "uya", 19 | "ratchet and clank 3", 20 | "up your arsenal", 21 | "multiplayer", 22 | "online", 23 | "server", 24 | "nodejs", 25 | "javascript", 26 | "emulator", 27 | "medius", 28 | "sce-rt", 29 | "rtime", 30 | "mas", 31 | "mls", 32 | "mps" 33 | ], 34 | "author": "hashsploit", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/hashsploit/clank/issues" 38 | }, 39 | "homepage": "https://github.com/hashsploit/clank", 40 | "dependencies": { 41 | "big-integer": "^1.6.48", 42 | "chalk": "^2.3.1", 43 | "sha1": "^1.1.1", 44 | "then-request": "^6.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let chalk = require("chalk"); 3 | let logger = require('./clank/logger.js'); 4 | let network = require('./clank/network.js'); 5 | let packets = require('./clank/packet.js'); 6 | let parameters = process.argv; 7 | 8 | require('./clank/util.js')(); 9 | 10 | global.name = "Clank"; 11 | global.version = "0.1.1"; 12 | 13 | if (parameters.length < 3) { 14 | console.error("Server configuration file must be specified e.g: \"nodejs server.js mas.json\" where mas.json is in /config/mas.json"); 15 | process.exit(-1); 16 | } 17 | 18 | let serverConfig = parameters[2]; 19 | 20 | try { 21 | global.config = require("./config/" + serverConfig); 22 | } catch (err) { 23 | console.error("Invalid server configuration file or does not exist in config/{0}".format(serverConfig)); 24 | process.exit(-1); 25 | } 26 | 27 | global.stopServer = function() { 28 | WebhookEvent.emit('shutdown', { 29 | "Action": "{0} ({1}) shutting down".format(global.config.mode.toUpperCase(), global.server_modes[global.config.mode]), 30 | "_Server Type": "{0}".format(global.config.mode.toUpperCase(), global.server_modes[global.config.mode]), 31 | "_Address": "{0}:{1}".format((global.config.address ? global.config.address : "*"), global.config.port), 32 | "_Capacity": global.config.capacity, 33 | "Icon": "green" 34 | }); 35 | network.disconnectAll(function() { 36 | logger.log("warn", "Shutting down server ..."); 37 | process.exit(0); 38 | }); 39 | } 40 | 41 | global.server_modes = { 42 | "mas": "Medius Authentication Server", 43 | "mls": "Medius Lobby Server", 44 | "mps": "Medius Proxy Server" 45 | }; 46 | 47 | if (!(global.config.mode in global.server_modes)) { 48 | console.error("Invalid server mode '{0}'! Type must be one of the following: {1}".format(Object.keys(global.server_modes).join(", "))); 49 | process.exit(-1); 50 | } 51 | 52 | 53 | let logo = [ 54 | "_|_|_| _| _|_| _| _| _| _|", 55 | "_| _| _| _| _|_| _| _| _|", 56 | "_| _| _|_|_|_| _| _| _| _|_|", 57 | "_| _| _| _| _| _|_| _| _|", 58 | "_|_|_| _|_|_|_| _| _| _| _| _| _| v{0}".format(global.version) 59 | ]; 60 | 61 | let bolt = [ 62 | " _________ ", 63 | "| \\/ \\/ |", 64 | "|_/\\___/\\_|", 65 | " |\\ \\ \\| ", 66 | " | \\ \\ | Mode : {0} ({1})".format(global.config.mode.toUpperCase(), global.server_modes[global.config.mode]), 67 | " |\\ \\ \\| Address : {0}:{1}".format((global.config.address ? global.config.address : "*"), global.config.port), 68 | " | \\ \\ | Capacity : {0}".format(global.config.capacity), 69 | " |\\ \\ \\| Whitelist : {0}".format((global.config.whitelist != null && global.config.whitelist.enabled != null) ? "[" + global.config.whitelist.players.join(", ") + "]" : "Off"), 70 | " | \\ \\ | Operators : {0}".format(global.config.operators != null ? "[" + global.config.operators.join(", ") + "]" : "None"), 71 | " |\\ \\ \\| ", 72 | " '-----' " 73 | ]; 74 | 75 | console.log(chalk["cyan"].bold(logo.join("\n")) + "\n"); 76 | console.log(chalk["cyan"].bold(bolt.join("\n")) + "\n"); 77 | 78 | logger.setLogLevel(global.config.log_level); 79 | logger.log("info", "Starting {0} v{1} ...".format(global.name, global.version), "cyan"); 80 | 81 | require("./clank/events/httpevent.js"); 82 | require("./clank/events/discordevent.js"); 83 | 84 | if (global.config.api.url) { 85 | logger.log("debug", "Broadcasting server start ...".format(global.config.api.url)); 86 | api("/start", global.config); 87 | } 88 | 89 | logger.log("info", "Emulating: {0} ({1})".format(global.config.mode, global.server_modes[global.config.mode])); 90 | 91 | packets.start(true); 92 | network.start(global.config.address, global.config.port); 93 | 94 | DiscordEvent.emit('start', { 95 | "Action": "{0} ({1}) started".format(global.config.mode.toUpperCase(), global.server_modes[global.config.mode]), 96 | "_Server Type": "{0}".format(global.config.mode.toUpperCase(), global.server_modes[global.config.mode]), 97 | "_Address": "{0}:{1}".format((global.config.address ? global.config.address : "*"), global.config.port), 98 | "_Capacity": global.config.capacity, 99 | "_Whitelist": "{0}".format((global.config.whitelist != null && global.config.whitelist.enabled != null) ? "[" + global.config.whitelist.players.join(", ") + "]" : "Off"), 100 | "_Operators": "{0}".format(global.config.operators != null ? "[" + global.config.operators.join(", ") + "]" : "None"), 101 | "Icon": "green" 102 | }); 103 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const fs = require('fs'); 3 | var crypto = require('crypto') 4 | 5 | const port = 10075; 6 | const host = '0.0.0.0'; 7 | 8 | const server = net.createServer(); 9 | server.listen(port, host, () => { 10 | console.log('Test TCP Server is running on port ' + port + '.'); 11 | }); 12 | 13 | let sockets = []; 14 | let state = 0; 15 | 16 | const JOIN_PACKET = [ 17 | "\x92\x40\x00\xf4\xf8\x7a\xf7\x34", 18 | "\x25\xc8\x9a\x6d\xa9\xdd\xeb\xab", 19 | "\xa8\x3c\xa6\xe6\xb4\x72\x6d\xef", 20 | "\x51\x23\x00\xde\xea\x43\xd5\x8f", 21 | "\x22\x50\x3f\xaf\x9c\x52\x96\x10", 22 | "\x7c\xa4\xbe\xa9\x57\x8a\xae\x49", 23 | "\x68\x06\x20\x73\xc6\x24\xa8\x07", 24 | "\xad\x44\xd2\x54\x29\x8d\x58\xb6", 25 | "\x3c\xda\x3b\xe4\x33\x8c\x57" 26 | ]; 27 | 28 | function toHexString(byteArray) { 29 | return Array.from(byteArray, function(byte) { 30 | return "" + ('0' + (byte & 0xFF).toString(16)).slice(-2); 31 | }).join(' '); 32 | } 33 | 34 | server.on('connection', function(sock) { 35 | 36 | console.log('CONNECTED: ' + sock.remoteAddress + ':' + sock.remotePort); 37 | sockets.push(sock); 38 | 39 | sock.on('data', function(data) { 40 | 41 | console.log('RECV: ' + sock.remoteAddress + ':' + sock.remotePort + ' > ' + toHexString(data)); 42 | 43 | var packet = []; 44 | 45 | if (state == 0) { 46 | 47 | packet = [ 48 | "\x93\x40\x00\x84\x95\x1f\xe2\xfb\x08\xe7", 49 | "\x8a\xa4\xc0\xbb\xf1\x23\xa5\xdd\xd9\xab\x9d\xd1\xf4\x65\x26\xa2", 50 | "\xff\x66\xc6\xf5\x98\x1d\xb9\x93\x83\x95\xb4\x4b\x61\x4b\xc3\x1f", 51 | "\xd3\x5e\xbc\x7a\x26\xd7\xdf\x58\xda\x05\xa4\x7b\x0c\x01\x0e\xfc", 52 | "\xa7\x6b\x62\x5e\xfe\xe1\xa6\x49\x59\x78\x52\xf9\x22" 53 | ]; 54 | state++; 55 | } else if (state == 1) { 56 | console.log("==== STATE 1"); 57 | packet = [ 58 | "\x94\x40\x00\x15\xe7\x62\x72\xa2\xa5\x95", 59 | "\xc9\x3b\x34\xa9\x68\xd6\xfe\xb7\x56\xa4\x2a\x6c\x95\xfb\x91\x2c", 60 | "\x94\xed\x7a\x9a\xa1\x27\x5a\xa6\xd2\xf6\x70\x61\xc4\x0e\xc8\x15", 61 | "\x44\xfe\x00\xb6\x16\x6a\x42\xa9\x40\xdb\x50\x38\x31\x02\xf8\x05", 62 | "\x94\xbd\xd9\x64\x93\xf7\xd3\xf1\x3b\x2c\xfa\x32\xe2" 63 | ]; 64 | state++; 65 | } else if (state == 2) { 66 | console.log("==== STATE 2"); 67 | packet = [ 68 | "\x8a\x32\x00\xbf\xeb\x09\x38\x9f\x54\x67", 69 | "\x22\x6d\x90\xaa\xb5\xd8\xe6\x6e\x1b\xf9\x5c\xd7\x96\x10\xc3\xae", 70 | "\x6a\xb4\x60\xde\x1a\xbc\x77\x02\xa4\x1c\x81\x0c\xd7\xf8\x90\xb1", 71 | "\xdb\xc7\xc3\x08\x52\x38\x34\xe3\x50\xd0\x74\xcb\xd9\xc7\x06" 72 | ]; 73 | state++; 74 | } else if (state == 3) { 75 | console.log("==== STATE 3"); 76 | packet = [ 77 | "\x8a\x1e\x00\xad\xef\xb4\x3a\x89\x2c\x74", 78 | "\x06\xf6\xe8\x9f\x51\x15\xba\x5d\x5f\xdc\xd7\x2d\x3d\x93\xb6\x12", 79 | "\x83\xe4\xa6\x9b\xff\xaa\x69\x49\x44\xa4\x33" 80 | ]; 81 | state++; 82 | } else if (state == 4) { 83 | console.log("==== STATE 4"); 84 | packet = [ 85 | "\x8a\xc6\x00\xfa\x10\x9a\x26\x19\x9f\x8c", 86 | "\x93\x3c\x35\x5f\xbd\x90\x21\xea\x65\x94\xd2\x4f\xa1\x19\xe2\x1f", 87 | "\x09\xb9\x13\x8c\xf6\x67\xa7\xb1\x57\xcd\xc4\xa0\xc1\xc4\x05\xf2", 88 | "\xed\xf0\x5b\x7e\x91\x70\x6b\xdc\x85\x70\xda\xe6\x26\xad\xe0\x9e", 89 | "\xbd\xcf\x9e\x31\xfd\x8f\x37\xeb\xe9\xc5\xc7\x54\x2a\x77\x27\x90", 90 | "\x73\xab\x37\xc8\x7b\x52\x7c\xda\xb5\x7f\x7c\xb4\xb3\x8c\xcd\x87", 91 | "\xd5\x8e\x57\x34\x6a\x34\x7b\x98\x8d\x48\xe6\x3e\x1a\xc2\x2c\x0c", 92 | "\x79\x74\x13\x7a\x35\x84\x56\x9e\xdc\x30\xbc\xa7\x13\xee\xdb\x5d", 93 | "\xc8\xf8\x74\x52\x7b\xe3\xf6\xcd\xe3\x1a\x19\xfd\x39\xc7\xc9\xd8", 94 | "\xbf\x89\xf5\x11\xaa\x75\x47\x00\x7f\x48\xc0\x13\x9f\x4b\x7d\xf9", 95 | "\x10\x00\x13\xb0\xa4\x6f\xee\xf9\xd2\x04\x35\xb4\xe3\x29\xf2\x54", 96 | "\x3d\x22\x5f\x4e\x62\x70\xc9\x90\x1c\xd5\x95\x6c\x45\x5e\xb4\xe1", 97 | "\x3d\x31\xcb\xee\x69\x8e\xa8\x59\x6d\x95\x55\xed\x1d\xb9\x6e\xb9", 98 | "\x4e\x6b\x08" 99 | ]; 100 | state++; 101 | } 102 | 103 | 104 | // Handle disconnect 105 | if (data == "\x01\x00\x00") { 106 | sock.end(); 107 | } 108 | 109 | if (packet != null && packet.length > 0) { 110 | var packetStr = packet.join(''); 111 | sock.write(packetStr); 112 | 113 | var myBuffer = []; 114 | var buffer = new Buffer(packetStr, 'binary'); 115 | for (var i=0; i ' + toHexString(myBuffer)); 120 | } 121 | 122 | }); 123 | 124 | // Add a 'close' event handler to this instance of socket 125 | sock.on('close', function(data) { 126 | let index = sockets.findIndex(function(o) { 127 | return o.remoteAddress === sock.remoteAddress && o.remotePort === sock.remotePort; 128 | }) 129 | if (index !== -1) { 130 | sockets.splice(index, 1); 131 | } 132 | console.log('DISCONNECTED: ' + sock.remoteAddress + ':' + sock.remotePort); 133 | }); 134 | }); 135 | --------------------------------------------------------------------------------