├── .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 |
--------------------------------------------------------------------------------