olgfopgkdg
This commit is contained in:
parent
53a12461ce
commit
5ed106b7d4
15 changed files with 3123 additions and 112 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
|||
node_modules/
|
||||
build/
|
||||
bundle/
|
||||
tHMM.ds
|
||||
server-stats.log
|
||||
config.json
|
14
Binato.ts
14
Binato.ts
|
@ -1,11 +1,11 @@
|
|||
import { ChatHistory } from "./server/ChatHistory";
|
||||
import compression from "compression";
|
||||
import config from "./config.json";
|
||||
import { ConsoleHelper } from "./ConsoleHelper";
|
||||
import express from "express";
|
||||
import { HandleRequest } from "./server/BanchoServer";
|
||||
import { readFileSync } from "fs";
|
||||
import { Registry, collectDefaultMetrics } from "prom-client";
|
||||
const config:any = JSON.parse(readFileSync(__dirname + "/config.json").toString());
|
||||
|
||||
const binatoApp:express.Application = express();
|
||||
|
||||
|
@ -48,13 +48,7 @@ binatoApp.use((req, res) => {
|
|||
break;
|
||||
|
||||
case "POST":
|
||||
// Make sure this address should respond to bancho requests
|
||||
// Bancho addresses: c, c1, c2, c3, c4, c5, c6, ce
|
||||
// Just looking for the first character being "c" *should* be enough
|
||||
if (req.headers.host != null && req.headers.host.split(".")[0][0] == "c")
|
||||
HandleRequest(req, res, packet);
|
||||
else
|
||||
res.status(400).send("400 | Bad Request!<br>Binato only accepts POST requests on Bancho subdomains.<hr>Binato");
|
||||
HandleRequest(req, res, packet);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -62,4 +56,6 @@ binatoApp.use((req, res) => {
|
|||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
binatoApp.listen(config.express.port, () => ConsoleHelper.printBancho(`Binato is up! Listening at port ${config.express.port}`));
|
2441
package-lock.json
generated
2441
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
@ -4,25 +4,36 @@
|
|||
"description": "",
|
||||
"main": "Binato.ts",
|
||||
"scripts": {
|
||||
"dev:run": "nodemon --watch './**/*.ts' Binato.ts"
|
||||
"dev:run": "nodemon --watch './**/*.ts' Binato.ts",
|
||||
"pack": "webpack",
|
||||
"build": "tsc --build",
|
||||
"_clean": "tsc --build --clean"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/node": "^18.11.9",
|
||||
"aes256": "^1.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"osu-packet": "^4.1.2",
|
||||
"prom-client": "^14.1.0",
|
||||
"redis": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"nodemon": "^2.0.20",
|
||||
"ts-node": "^10.9.1"
|
||||
"ts-loader": "^9.4.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import config from "../config.json";
|
||||
import { ConsoleHelper } from "../ConsoleHelper";
|
||||
import { Database } from "./objects/Database";
|
||||
import { UserArray } from "./objects/UserArray";
|
||||
import { LatLng } from "./objects/LatLng";
|
||||
import { LoginProcess } from "./LoginProcess";
|
||||
import { Packets } from "./enums/Packets";
|
||||
import { RedisClientType, createClient } from "redis";
|
||||
import { replaceAll } from "./Util";
|
||||
import { readFileSync } from "fs";
|
||||
import { RedisClientType, createClient } from "redis";
|
||||
import { Request, Response } from "express";
|
||||
import { UserArray } from "./objects/UserArray";
|
||||
import { User } from "./objects/User";
|
||||
import * as osu from "osu-packet";
|
||||
const config:any = JSON.parse(readFileSync(__dirname + "/config.json").toString());
|
||||
// TODO: Port osu-packet to TypeScript
|
||||
const osu = require("osu-packet");
|
||||
|
||||
/*const
|
||||
loginHandler = require("./loginHandler.js"),
|
||||
|
@ -59,10 +62,12 @@ if (config.redis.enabled) {
|
|||
subscribeToChannel("binato:update_user_stats", (message) => {
|
||||
if (typeof(message) === "string") {
|
||||
const user = users.getById(parseInt(message));
|
||||
// Update user info
|
||||
user.updateUserInfo(true);
|
||||
if (user != null) {
|
||||
// Update user info
|
||||
user.updateUserInfo(true);
|
||||
|
||||
ConsoleHelper.printRedis(`Score submission stats update request received for ${user.username}`);
|
||||
ConsoleHelper.printRedis(`Score submission stats update request received for ${user.username}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
@ -71,12 +76,11 @@ if (config.redis.enabled) {
|
|||
// User timeout interval
|
||||
setInterval(() => {
|
||||
for (let User of users.getIterableItems()) {
|
||||
if (User.id == 3) continue; // Ignore the bot
|
||||
// Bot: :(
|
||||
if (User.uuid == "bot") continue; // Ignore the bot
|
||||
|
||||
// Logout this user, they're clearly gone.
|
||||
if (Date.now() >= User.timeoutTime)
|
||||
Logout(User);
|
||||
// if (Date.now() >= User.timeoutTime)
|
||||
// Logout(User);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
|
@ -101,7 +105,7 @@ setInterval(() => {
|
|||
//Streams.addStream("multiplayer_lobby", false);
|
||||
|
||||
// Include packets
|
||||
const ChangeAction = require("./Packets/ChangeAction.js"),
|
||||
/*const ChangeAction = require("./Packets/ChangeAction.js"),
|
||||
SendPublicMessage = require("./Packets/SendPublicMessage.js"),
|
||||
Logout = require("./Packets/Logout.js"),
|
||||
Spectator = require("./Spectator.js"),
|
||||
|
@ -118,30 +122,37 @@ const ChangeAction = require("./Packets/ChangeAction.js"),
|
|||
MultiplayerInvite = require("./Packets/MultiplayerInvite.js"),
|
||||
TourneyMatchSpecialInfo = require("./Packets/TourneyMatchSpecialInfo.js"),
|
||||
TourneyMatchJoinChannel = require("./Packets/TourneyMatchSpecialInfo.js"),
|
||||
TourneyMatchLeaveChannel = require("./Packets/TourneyLeaveMatchChannel.js");
|
||||
TourneyMatchLeaveChannel = require("./Packets/TourneyLeaveMatchChannel.js");*/
|
||||
|
||||
// A class for managing everything multiplayer
|
||||
const multiplayerManager:MultiplayerManager = new MultiplayerManager();
|
||||
//const multiplayerManager:MultiplayerManager = new MultiplayerManager();
|
||||
|
||||
const EMPTY_BUFFER = Buffer.alloc(0);
|
||||
|
||||
export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
|
||||
// Remove headers we don't need for Bancho
|
||||
res.removeHeader('X-Powered-By');
|
||||
res.removeHeader('Date');
|
||||
|
||||
// Get the client's token string and request data
|
||||
const requestTokenString:string | undefined = req.header("osu-token"),
|
||||
requestData:Buffer = packet;
|
||||
|
||||
// Server's response
|
||||
let responseData:Buffer;
|
||||
const requestTokenString:string | undefined = req.header("osu-token");
|
||||
|
||||
// Check if the user is logged in
|
||||
if (requestTokenString == null) {
|
||||
// Client doesn't have a token yet, let's auth them!
|
||||
const userData = parseUserData(requestData);
|
||||
ConsoleHelper.printBancho(`New client connection. [User: ${userData.username}]`);
|
||||
await loginHandler(req, res, userData);
|
||||
// Only do this if we're absolutely sure that we're connected to the DB
|
||||
if (DB.connected) {
|
||||
// Client doesn't have a token yet, let's auth them!
|
||||
|
||||
await LoginProcess(req, res, packet, DB, users);
|
||||
DB.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [users.getLength() - 1]);
|
||||
}
|
||||
} else {
|
||||
let responseData:Buffer | string = EMPTY_BUFFER;
|
||||
|
||||
// Client has a token, let's see what they want.
|
||||
try {
|
||||
// Get the current user
|
||||
const PacketUser:User = users.getByToken(requestTokenString);
|
||||
const PacketUser:User | undefined = users.getByToken(requestTokenString);
|
||||
|
||||
// Make sure the client's token isn't invalid
|
||||
if (PacketUser != null) {
|
||||
|
@ -149,13 +160,13 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
|
|||
PacketUser.timeoutTime = Date.now() + 60000;
|
||||
|
||||
// Create a new osu! packet reader
|
||||
const osuPacketReader = new osu.Client.Reader(requestData);
|
||||
const osuPacketReader = new osu.Client.Reader(packet);
|
||||
// Parse current bancho packet
|
||||
const PacketData = osuPacketReader.Parse();
|
||||
|
||||
// Go through each packet sent by the client
|
||||
for (let CurrentPacket of PacketData) {
|
||||
switch (CurrentPacket.id) {
|
||||
/*switch (CurrentPacket.id) {
|
||||
case Packets.Client_ChangeAction:
|
||||
ChangeAction(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
@ -184,136 +195,136 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
|
|||
Spectator.stopSpectatingUser(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_sendPrivateMessage:
|
||||
case Packets.Client_SendPrivateMessage:
|
||||
SendPrivateMessage(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_joinLobby:
|
||||
global.MultiplayerManager.userEnterLobby(PacketUser);
|
||||
case Packets.Client_JoinLobby:
|
||||
multiplayerManager.userEnterLobby(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_partLobby:
|
||||
global.MultiplayerManager.userLeaveLobby(PacketUser);
|
||||
case Packets.Client_PartLobby:
|
||||
multiplayerManager.userLeaveLobby(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_createMatch:
|
||||
await global.MultiplayerManager.createMultiplayerMatch(PacketUser, CurrentPacket.data);
|
||||
case Packets.Client_CreateMatch:
|
||||
await multiplayerManager.createMultiplayerMatch(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_joinMatch:
|
||||
global.MultiplayerManager.joinMultiplayerMatch(PacketUser, CurrentPacket.data);
|
||||
case Packets.Client_JoinMatch:
|
||||
multiplayerManager.joinMultiplayerMatch(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchChangeSlot:
|
||||
case Packets.Client_MatchChangeSlot:
|
||||
PacketUser.currentMatch.moveToSlot(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchReady:
|
||||
case Packets.Client_MatchReady:
|
||||
PacketUser.currentMatch.setStateReady(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchChangeSettings:
|
||||
case Packets.Client_MatchChangeSettings:
|
||||
await PacketUser.currentMatch.updateMatch(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchNotReady:
|
||||
case Packets.Client_MatchNotReady:
|
||||
PacketUser.currentMatch.setStateNotReady(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_partMatch:
|
||||
await global.MultiplayerManager.leaveMultiplayerMatch(PacketUser);
|
||||
case Packets.Client_PartMatch:
|
||||
await multiplayerManager.leaveMultiplayerMatch(PacketUser);
|
||||
break;
|
||||
|
||||
// Also handles user kick if the slot has a user
|
||||
case Packets.Client_matchLock:
|
||||
case Packets.Client_MatchLock:
|
||||
PacketUser.currentMatch.lockMatchSlot(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchNoBeatmap:
|
||||
case Packets.Client_MatchNoBeatmap:
|
||||
PacketUser.currentMatch.missingBeatmap(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchSkipRequest:
|
||||
case Packets.Client_MatchSkipRequest:
|
||||
PacketUser.currentMatch.matchSkip(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchHasBeatmap:
|
||||
case Packets.Client_MatchHasBeatmap:
|
||||
PacketUser.currentMatch.notMissingBeatmap(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchTransferHost:
|
||||
case Packets.Client_MatchTransferHost:
|
||||
PacketUser.currentMatch.transferHost(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchChangeMods:
|
||||
case Packets.Client_MatchChangeMods:
|
||||
PacketUser.currentMatch.updateMods(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchStart:
|
||||
case Packets.Client_MatchStart:
|
||||
PacketUser.currentMatch.startMatch();
|
||||
break;
|
||||
|
||||
case Packets.Client_matchLoadComplete:
|
||||
case Packets.Client_MatchLoadComplete:
|
||||
PacketUser.currentMatch.matchPlayerLoaded(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchComplete:
|
||||
case Packets.Client_MatchComplete:
|
||||
await PacketUser.currentMatch.onPlayerFinishMatch(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchScoreUpdate:
|
||||
case Packets.Client_MatchScoreUpdate:
|
||||
PacketUser.currentMatch.updatePlayerScore(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchFailed:
|
||||
case Packets.Client_MatchFailed:
|
||||
PacketUser.currentMatch.matchFailed(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_matchChangeTeam:
|
||||
case Packets.Client_MatchChangeTeam:
|
||||
PacketUser.currentMatch.changeTeam(PacketUser);
|
||||
break;
|
||||
|
||||
case Packets.Client_channelJoin:
|
||||
case Packets.Client_ChannelJoin:
|
||||
ChannelJoin(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_channelPart:
|
||||
case Packets.Client_ChannelPart:
|
||||
ChannelPart(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_setAwayMessage:
|
||||
case Packets.Client_SetAwayMessage:
|
||||
SetAwayMessage(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_friendAdd:
|
||||
case Packets.Client_FriendAdd:
|
||||
AddFriend(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_friendRemove:
|
||||
case Packets.Client_FriendRemove:
|
||||
RemoveFriend(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_userStatsRequest:
|
||||
case Packets.Client_UserStatsRequest:
|
||||
UserStatsRequest(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_specialMatchInfoRequest:
|
||||
case Packets.Client_SpecialMatchInfoRequest:
|
||||
TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_specialJoinMatchChannel:
|
||||
case Packets.Client_SpecialJoinMatchChannel:
|
||||
TourneyMatchJoinChannel(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_specialLeaveMatchChannel:
|
||||
case Packets.Client_SpecialLeaveMatchChannel:
|
||||
TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_invite:
|
||||
case Packets.Client_Invite:
|
||||
MultiplayerInvite(PacketUser, CurrentPacket.data);
|
||||
break;
|
||||
|
||||
case Packets.Client_userPresenceRequest:
|
||||
case Packets.Client_UserPresenceRequest:
|
||||
UserPresence(PacketUser, PacketUser.id); // Can't really think of a way to generalize this?
|
||||
break;
|
||||
|
||||
|
@ -323,22 +334,22 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
|
|||
// Print out unimplemented packet
|
||||
console.dir(CurrentPacket);
|
||||
break;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
responseData = PacketUser.queue;
|
||||
PacketUser.clearQueue();
|
||||
} else {
|
||||
// User's token is invlid, force a reconnect
|
||||
consoleHelper.printBancho(`Forced client re-login (Token is invalid)`);
|
||||
responseData = bakedResponses("reconnect");
|
||||
// Only do this if we're absolutely sure that we're connected to the DB
|
||||
if (DB.connected) {
|
||||
// User's token is invlid, force a reconnect
|
||||
ConsoleHelper.printBancho(`Forced client re-login (Token is invalid)`);
|
||||
responseData = "\u0005\u0000\u0000\u0004\u0000\u0000\u0000<30><30><EFBFBD><EFBFBD>\u0018\u0000\u0000\u0011\u0000\u0000\u0000\u000b\u000fReconnecting...";
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
// Only send the headers that we absolutely have to
|
||||
res.removeHeader('X-Powered-By');
|
||||
res.removeHeader('Date');
|
||||
res.writeHead(200, {
|
||||
"Connection": "keep-alive",
|
||||
"Keep-Alive": "timeout=5, max=100",
|
||||
|
@ -347,4 +358,4 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) {
|
|||
res.end(responseData);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
264
server/Country.ts
Normal file
264
server/Country.ts
Normal file
|
@ -0,0 +1,264 @@
|
|||
const countryCodes = {
|
||||
LV: 132,
|
||||
AD: 3,
|
||||
LT: 130,
|
||||
KM: 116,
|
||||
QA: 182,
|
||||
VA: 0,
|
||||
PK: 173,
|
||||
KI: 115,
|
||||
SS: 0,
|
||||
KH: 114,
|
||||
NZ: 166,
|
||||
TO: 215,
|
||||
KZ: 122,
|
||||
GA: 76,
|
||||
BW: 35,
|
||||
AX: 247,
|
||||
GE: 79,
|
||||
UA: 222,
|
||||
CR: 50,
|
||||
AE: 0,
|
||||
NE: 157,
|
||||
ZA: 240,
|
||||
SK: 196,
|
||||
BV: 34,
|
||||
SH: 0,
|
||||
PT: 179,
|
||||
SC: 189,
|
||||
CO: 49,
|
||||
GP: 86,
|
||||
GY: 93,
|
||||
CM: 47,
|
||||
TJ: 211,
|
||||
AF: 5,
|
||||
IE: 101,
|
||||
AL: 8,
|
||||
BG: 24,
|
||||
JO: 110,
|
||||
MU: 149,
|
||||
PM: 0,
|
||||
LA: 0,
|
||||
IO: 104,
|
||||
KY: 121,
|
||||
SA: 187,
|
||||
KN: 0,
|
||||
OM: 167,
|
||||
CY: 54,
|
||||
BQ: 0,
|
||||
BT: 33,
|
||||
WS: 236,
|
||||
ES: 67,
|
||||
LR: 128,
|
||||
RW: 186,
|
||||
AQ: 12,
|
||||
PW: 180,
|
||||
JE: 250,
|
||||
TN: 214,
|
||||
ZW: 243,
|
||||
JP: 111,
|
||||
BB: 20,
|
||||
VN: 233,
|
||||
HN: 96,
|
||||
KP: 0,
|
||||
WF: 235,
|
||||
EC: 62,
|
||||
HU: 99,
|
||||
GF: 80,
|
||||
GQ: 87,
|
||||
TW: 220,
|
||||
MC: 135,
|
||||
BE: 22,
|
||||
PN: 176,
|
||||
SZ: 205,
|
||||
CZ: 55,
|
||||
LY: 0,
|
||||
IN: 103,
|
||||
FM: 0,
|
||||
PY: 181,
|
||||
PH: 172,
|
||||
MN: 142,
|
||||
GG: 248,
|
||||
CC: 39,
|
||||
ME: 242,
|
||||
DO: 60,
|
||||
KR: 0,
|
||||
PL: 174,
|
||||
MT: 148,
|
||||
MM: 141,
|
||||
AW: 17,
|
||||
MV: 150,
|
||||
BD: 21,
|
||||
NR: 164,
|
||||
AT: 15,
|
||||
GW: 92,
|
||||
FR: 74,
|
||||
LI: 126,
|
||||
CF: 41,
|
||||
DZ: 61,
|
||||
MA: 134,
|
||||
VG: 0,
|
||||
NC: 156,
|
||||
IQ: 105,
|
||||
BN: 0,
|
||||
BF: 23,
|
||||
BO: 30,
|
||||
GB: 77,
|
||||
CU: 51,
|
||||
LU: 131,
|
||||
YT: 238,
|
||||
NO: 162,
|
||||
SM: 198,
|
||||
GL: 83,
|
||||
IS: 107,
|
||||
AO: 11,
|
||||
MH: 138,
|
||||
SE: 191,
|
||||
ZM: 241,
|
||||
FJ: 70,
|
||||
SL: 197,
|
||||
CH: 43,
|
||||
RU: 0,
|
||||
CW: 0,
|
||||
CX: 53,
|
||||
TF: 208,
|
||||
NL: 161,
|
||||
AU: 16,
|
||||
FI: 69,
|
||||
MS: 147,
|
||||
GH: 81,
|
||||
BY: 36,
|
||||
IL: 102,
|
||||
VC: 0,
|
||||
NG: 159,
|
||||
HT: 98,
|
||||
LS: 129,
|
||||
MR: 146,
|
||||
YE: 237,
|
||||
MP: 144,
|
||||
SX: 0,
|
||||
RE: 183,
|
||||
RO: 184,
|
||||
NP: 163,
|
||||
CG: 0,
|
||||
FO: 73,
|
||||
CI: 0,
|
||||
TH: 210,
|
||||
HK: 94,
|
||||
TK: 212,
|
||||
XK: 0,
|
||||
DM: 59,
|
||||
LC: 0,
|
||||
ID: 100,
|
||||
MG: 137,
|
||||
JM: 109,
|
||||
IT: 108,
|
||||
CA: 38,
|
||||
TZ: 221,
|
||||
GI: 82,
|
||||
KG: 113,
|
||||
NU: 165,
|
||||
TV: 219,
|
||||
LB: 124,
|
||||
SY: 0,
|
||||
PR: 177,
|
||||
NI: 160,
|
||||
KE: 112,
|
||||
MO: 0,
|
||||
SR: 201,
|
||||
VI: 0,
|
||||
SV: 203,
|
||||
HM: 0,
|
||||
CD: 0,
|
||||
BI: 26,
|
||||
BM: 28,
|
||||
MW: 151,
|
||||
TM: 213,
|
||||
GT: 90,
|
||||
AG: 0,
|
||||
UM: 0,
|
||||
US: 225,
|
||||
AR: 13,
|
||||
DJ: 57,
|
||||
KW: 120,
|
||||
MY: 153,
|
||||
FK: 71,
|
||||
EG: 64,
|
||||
BA: 0,
|
||||
CN: 48,
|
||||
GN: 85,
|
||||
PS: 178,
|
||||
SO: 200,
|
||||
IM: 249,
|
||||
GS: 0,
|
||||
BR: 31,
|
||||
GM: 84,
|
||||
PF: 170,
|
||||
PA: 168,
|
||||
PG: 171,
|
||||
BH: 25,
|
||||
TG: 209,
|
||||
GU: 91,
|
||||
CK: 45,
|
||||
MF: 252,
|
||||
VE: 230,
|
||||
CL: 46,
|
||||
TR: 217,
|
||||
UG: 223,
|
||||
GD: 78,
|
||||
TT: 218,
|
||||
TL: 0,
|
||||
MD: 0,
|
||||
MK: 0,
|
||||
ST: 202,
|
||||
CV: 52,
|
||||
MQ: 145,
|
||||
GR: 88,
|
||||
HR: 97,
|
||||
BZ: 37,
|
||||
UZ: 227,
|
||||
DK: 58,
|
||||
SN: 199,
|
||||
ET: 68,
|
||||
VU: 234,
|
||||
ER: 66,
|
||||
BJ: 27,
|
||||
LK: 127,
|
||||
NA: 155,
|
||||
AS: 14,
|
||||
SG: 192,
|
||||
PE: 169,
|
||||
IR: 0,
|
||||
MX: 152,
|
||||
TD: 207,
|
||||
AZ: 18,
|
||||
AM: 9,
|
||||
BL: 0,
|
||||
SJ: 195,
|
||||
SB: 188,
|
||||
NF: 158,
|
||||
RS: 239,
|
||||
DE: 56,
|
||||
EH: 65,
|
||||
EE: 63,
|
||||
SD: 190,
|
||||
ML: 140,
|
||||
TC: 206,
|
||||
MZ: 154,
|
||||
BS: 32,
|
||||
UY: 226,
|
||||
SI: 194,
|
||||
AI: 7
|
||||
};
|
||||
|
||||
const countryCodeKeys = Object.keys(countryCodes);
|
||||
|
||||
export function getCountryID(code:string) : number {
|
||||
// Get id of a country from a 2 char code
|
||||
/*const upperCode:string = code.toUpperCase();
|
||||
if (code in countryCodes) {
|
||||
return countryCodes[upperCode];
|
||||
}*/
|
||||
|
||||
return 0;
|
||||
}
|
227
server/LoginProcess.ts
Normal file
227
server/LoginProcess.ts
Normal file
|
@ -0,0 +1,227 @@
|
|||
import { ConsoleHelper } from "../ConsoleHelper";
|
||||
import { Database } from "./objects/Database";
|
||||
import fetch from "node-fetch";
|
||||
import { getCountryID } from "./Country";
|
||||
import { generateSession } from "./Util";
|
||||
import { LatLng } from "./objects/LatLng";
|
||||
import { LoginInfo } from "./objects/LoginInfo";
|
||||
import { Logout } from "./packets/Logout";
|
||||
import { pbkdf2 } from "crypto";
|
||||
import { readFileSync } from "fs";
|
||||
import { Request, Response } from "express";
|
||||
import { UserArray } from "./objects/UserArray";
|
||||
import { User } from "./objects/User";
|
||||
const config:any = JSON.parse(readFileSync(__dirname + "/config.json").toString());
|
||||
const { decrypt: aesDecrypt } = require("aes256");
|
||||
const osu = require("osu-packet");
|
||||
|
||||
function incorrectLoginResponse() {
|
||||
const osuPacketWriter = new osu.Bancho.Writer;
|
||||
osuPacketWriter.LoginReply(-1);
|
||||
return [
|
||||
osuPacketWriter.toBuffer,
|
||||
{
|
||||
'cho-protocol': 19,
|
||||
'Connection': 'keep-alive',
|
||||
'Keep-Alive': 'timeout=5, max=100',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function requiredPWChangeResponse() {
|
||||
const osuPacketWriter = new osu.Bancho.Writer;
|
||||
osuPacketWriter.Announce("As part of migration to a new password system you are required to change your password. Please log in on the website and change your password.");
|
||||
osuPacketWriter.LoginReply(-1);
|
||||
return [
|
||||
osuPacketWriter.toBuffer,
|
||||
{
|
||||
'cho-protocol': 19,
|
||||
'Connection': 'keep-alive',
|
||||
'Keep-Alive': 'timeout=5, max=100',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
enum LoginTypes {
|
||||
CURRENT,
|
||||
OLD_MD5,
|
||||
OLD_AES
|
||||
}
|
||||
|
||||
function TestLogin(loginInfo:LoginInfo | undefined, database:Database) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Check if there is any login information provided
|
||||
if (loginInfo == null) return resolve(incorrectLoginResponse());
|
||||
|
||||
const userDBData:any = await database.query("SELECT * FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
|
||||
|
||||
// Make sure a user was found in the database
|
||||
if (userDBData == null) return resolve(incorrectLoginResponse());
|
||||
// Make sure the username is the same as the login info
|
||||
if (userDBData.username !== loginInfo.username) return resolve(incorrectLoginResponse());
|
||||
/*
|
||||
1: Old MD5 password
|
||||
2: Old AES password
|
||||
*/
|
||||
if (userDBData.has_old_password === LoginTypes.OLD_MD5) {
|
||||
if (userDBData.password_hash !== loginInfo.password)
|
||||
return resolve(incorrectLoginResponse());
|
||||
|
||||
return resolve(requiredPWChangeResponse());
|
||||
} else if (userDBData.has_old_password === LoginTypes.OLD_AES) {
|
||||
if (aesDecrypt(config.database.key, userDBData.password_hash) !== loginInfo.password)
|
||||
return resolve(resolve(incorrectLoginResponse()));
|
||||
|
||||
return resolve(requiredPWChangeResponse());
|
||||
} else {
|
||||
pbkdf2(loginInfo.password, userDBData.password_salt, config.database.pbkdf2.itterations, config.database.pbkdf2.keylength, "sha512", (err, derivedKey) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
} else {
|
||||
if (derivedKey.toString("hex") !== userDBData.password_hash)
|
||||
return resolve(incorrectLoginResponse());
|
||||
|
||||
return resolve(undefined); // We good
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function LoginProcess(req:Request, res:Response, packet:Buffer, database:Database, users:UserArray) {
|
||||
const loginInfo = LoginInfo.From(packet);
|
||||
const loginStartTime = Date.now();
|
||||
|
||||
const loginCheck:any = await TestLogin(loginInfo, database);
|
||||
if (loginCheck != null) {
|
||||
res.writeHead(200, loginCheck[1]);
|
||||
return res.end(loginCheck[0]);
|
||||
}
|
||||
|
||||
if (loginInfo == null)
|
||||
return;
|
||||
|
||||
ConsoleHelper.printBancho(`New client connection. [User: ${loginInfo.username}]`);
|
||||
|
||||
// Get users IP for getting location
|
||||
// Get cloudflare requestee IP first
|
||||
let requestIP = req.get("cf-connecting-ip");
|
||||
|
||||
// Get IP of requestee since we are probably behind a reverse proxy
|
||||
if (requestIP == null)
|
||||
requestIP = req.get("X-Real-IP");
|
||||
|
||||
// Just get the requestee IP (we are not behind a reverse proxy)
|
||||
// if (requestIP == null)
|
||||
// requestIP = req.remote_addr;
|
||||
|
||||
// Make sure requestIP is never null
|
||||
if (requestIP == null)
|
||||
requestIP = "";
|
||||
|
||||
|
||||
let userCountryCode:string, userLocation:LatLng;
|
||||
// Check if it is a local or null IP
|
||||
if (!requestIP.includes("192.168.") && !requestIP.includes("127.0.") && requestIP != "") {
|
||||
// Set location to null island
|
||||
userCountryCode = "XX";
|
||||
userLocation = new LatLng(0, 0);
|
||||
} else {
|
||||
// Get user's location using zxq
|
||||
const userLocationRequest = await fetch(`https://ip.zxq.co/${requestIP}`);
|
||||
const userLocationData:any = await userLocationRequest.json();
|
||||
const userLatLng:Array<string> = userLocationData.loc.split(",");
|
||||
userCountryCode = userLocationData.country;
|
||||
userLocation = new LatLng(parseFloat(userLatLng[0]), parseFloat(userLatLng[1]));
|
||||
}
|
||||
|
||||
// Get information about the user from the database
|
||||
const userDB = await database.query("SELECT id FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
|
||||
|
||||
// Create a token for the client
|
||||
const newClientToken:string = await generateSession();
|
||||
const isTourneyClient = loginInfo.version.includes("tourney");
|
||||
|
||||
// Make sure user is not already connected, kick off if so.
|
||||
const connectedUser = users.getByUsername(loginInfo.username);
|
||||
if (connectedUser != null && !isTourneyClient && !connectedUser.isTourneyUser) {
|
||||
Logout(connectedUser, database);
|
||||
}
|
||||
|
||||
// Retreive the newly created user
|
||||
const newUser:User = users.add(newClientToken, new User(userDB.id, loginInfo.username, newClientToken, database));
|
||||
// Set tourney client flag
|
||||
newUser.isTourneyUser = isTourneyClient;
|
||||
newUser.location = userLocation;
|
||||
|
||||
// Get user's data from the database
|
||||
newUser.updateUserInfo();
|
||||
|
||||
try {
|
||||
// Save the country id for the same reason as above
|
||||
newUser.countryID = getCountryID(userCountryCode);
|
||||
|
||||
// We're ready to start putting together a login packet
|
||||
// Create an osu! Packet writer
|
||||
let osuPacketWriter = new osu.Bancho.Writer;
|
||||
|
||||
// The reply id is the user's id in any other case than an error in which case negative numbers are used
|
||||
osuPacketWriter.LoginReply(newUser.id);
|
||||
// Current bancho protocol version. Defined in Binato.js
|
||||
osuPacketWriter.ProtocolNegotiation(19);
|
||||
// Permission level 4 is osu!supporter
|
||||
osuPacketWriter.LoginPermissions(4);
|
||||
|
||||
// After sending the user their friends list send them the online users
|
||||
//UserPresenceBundle(newUser);
|
||||
|
||||
// Set title screen image
|
||||
//osuPacketWriter.TitleUpdate("http://puu.sh/jh7t7/20c04029ad.png|https://osu.ppy.sh/news/123912240253");
|
||||
|
||||
// Add user panel data packets
|
||||
//UserPresence(newUser, newUser.id);
|
||||
//StatusUpdate(newUser, newUser.id);
|
||||
|
||||
// peppy pls, why
|
||||
osuPacketWriter.ChannelListingComplete();
|
||||
|
||||
// Add user to #osu
|
||||
osuPacketWriter.ChannelJoinSuccess("#osu");
|
||||
//if (!Streams.isUserInStream("#osu", newUser.uuid))
|
||||
// Streams.addUserToStream("#osu", newUser.uuid);
|
||||
|
||||
// List all channels out to the client
|
||||
/*for (let i = 0; i < global.channels.length; i++) {
|
||||
osuPacketWriter.ChannelAvailable({
|
||||
channelName: global.channels[i].channelName,
|
||||
channelTopic: global.channels[i].channelTopic,
|
||||
channelUserCount: global.channels[i].channelUserCount
|
||||
});
|
||||
}*/
|
||||
|
||||
// Construct user's friends list
|
||||
const userFriends = await database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]);
|
||||
let friendsArray = [];
|
||||
for (let i = 0; i < userFriends.length; i++) {
|
||||
friendsArray.push(userFriends[i].friendsWith);
|
||||
}
|
||||
// Send user's friends list
|
||||
osuPacketWriter.FriendsList(friendsArray);
|
||||
|
||||
osuPacketWriter.Announce(`Welcome back ${loginInfo.username}!`);
|
||||
|
||||
res.removeHeader('X-Powered-By');
|
||||
res.removeHeader('Date');
|
||||
// Complete login
|
||||
res.writeHead(200, {
|
||||
"cho-token": newUser.uuid,
|
||||
"Connection": "keep-alive",
|
||||
"Keep-Alive": "timeout=5, max=100",
|
||||
});
|
||||
res.end(osuPacketWriter.toBuffer, () => {
|
||||
ConsoleHelper.printBancho(`User login finished, took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
3
server/MultiplayerManager.ts
Normal file
3
server/MultiplayerManager.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export abstract class MultiplayerManager {
|
||||
|
||||
}
|
|
@ -5,4 +5,22 @@ function escapeRegExp(string:string) {
|
|||
|
||||
export function replaceAll(inputString:string, toReplace:string, toReplaceWith:string) {
|
||||
return inputString.replace(`/:${escapeRegExp(toReplace)}:/g`, toReplaceWith);
|
||||
}
|
||||
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
export function generateSession() : Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
randomBytes(12, (err, buf) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(buf.toString("hex"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function generateSessionSync() : string {
|
||||
return randomBytes(12).toString("hex");
|
||||
}
|
|
@ -5,6 +5,8 @@ export class Database {
|
|||
private connectionPool:Pool;
|
||||
private static readonly CONNECTION_LIMIT = 128;
|
||||
|
||||
public connected:boolean = false;
|
||||
|
||||
public constructor(databaseAddress:string, databasePort:number = 3306, databaseUsername:string, databasePassword:string, databaseName:string, connectedCallback:Function) {
|
||||
this.connectionPool = createPool({
|
||||
connectionLimit: Database.CONNECTION_LIMIT,
|
||||
|
@ -16,22 +18,32 @@ export class Database {
|
|||
});
|
||||
|
||||
const classCreationTime:number = Date.now();
|
||||
let lastQueryFinished = true;
|
||||
const connectionCheckInterval = setInterval(() => {
|
||||
this.query("SELECT name FROM osu_info LIMIT 1")
|
||||
if (lastQueryFinished) {
|
||||
lastQueryFinished = false;
|
||||
this.query("SELECT name FROM osu_info LIMIT 1")
|
||||
.then(data => {
|
||||
ConsoleHelper.printBancho(`Connected to database. Took ${Date.now() - classCreationTime}ms`);
|
||||
clearInterval(connectionCheckInterval);
|
||||
if (!this.connected) {
|
||||
this.connected = true;
|
||||
ConsoleHelper.printBancho(`Connected to database. Took ${Date.now() - classCreationTime}ms`);
|
||||
clearInterval(connectionCheckInterval);
|
||||
lastQueryFinished = true;
|
||||
|
||||
connectedCallback();
|
||||
connectedCallback();
|
||||
}
|
||||
})
|
||||
.catch(err => {});
|
||||
}, 17); // Roughly 6 times per sec
|
||||
.catch(err => {
|
||||
lastQueryFinished = true;
|
||||
});
|
||||
}
|
||||
}, 16);
|
||||
}
|
||||
|
||||
public query(query = "", data?:Array<any>) {
|
||||
const limited = query.includes("LIMIT 1");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
this.connectionPool.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
|
33
server/objects/LoginInfo.ts
Normal file
33
server/objects/LoginInfo.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
export class LoginInfo {
|
||||
public username:string;
|
||||
public password:string;
|
||||
public version:string;
|
||||
public timeOffset:number;
|
||||
// TODO: Parse client data
|
||||
public clientData:string;
|
||||
|
||||
private constructor(username:string, password:string, version:string, timeOffset:number, clientData:string) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.version = version;
|
||||
this.timeOffset = timeOffset;
|
||||
this.clientData = clientData;
|
||||
}
|
||||
|
||||
public static From(data:Buffer | string) : LoginInfo | undefined {
|
||||
if (data instanceof Buffer) {
|
||||
data = data.toString();
|
||||
}
|
||||
|
||||
const loginData:Array<string> = data.split("\n");
|
||||
const extraData:Array<string> = loginData[2].split("|");
|
||||
|
||||
if (loginData.length !== 4 || extraData.length !== 5) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: Parse client data
|
||||
|
||||
return new LoginInfo(loginData[0], loginData[1], extraData[0], parseInt(extraData[1]), extraData[3].split(":")[2]);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Database } from "./Database";
|
||||
import { LatLng } from "./LatLng";
|
||||
import { RankingModes } from "../enums/RankingModes";
|
||||
const StatusUpdate = require("./Packets/StatusUpdate.js");
|
||||
//const StatusUpdate = require("./Packets/StatusUpdate.js");
|
||||
|
||||
const rankingModes = [
|
||||
"pp_raw",
|
||||
|
@ -89,9 +89,9 @@ export class User {
|
|||
|
||||
// Gets the user's score information from the database and caches it
|
||||
async updateUserInfo(forceUpdate:boolean = false) : Promise<void> {
|
||||
const userScoreDB:any = await this.dbConnection.query("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]);
|
||||
const userScoreDB = await this.dbConnection.query("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]);
|
||||
const mappedRankingMode = rankingModes[this.rankingMode];
|
||||
const userRankDB:any = await this.dbConnection.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]);
|
||||
const userRankDB = await this.dbConnection.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]);
|
||||
|
||||
if (userScoreDB == null || userRankDB == null) throw "fuck";
|
||||
|
||||
|
@ -132,7 +132,7 @@ export class User {
|
|||
else this.pp = 0;
|
||||
|
||||
if (userScoreUpdate || forceUpdate) {
|
||||
StatusUpdate(this, this.id);
|
||||
//StatusUpdate(this, this.id);
|
||||
}
|
||||
}
|
||||
}
|
7
server/packets/Logout.ts
Normal file
7
server/packets/Logout.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { ConsoleHelper } from "../../ConsoleHelper";
|
||||
import { Database } from "../objects/Database";
|
||||
import { User } from "../objects/User";
|
||||
|
||||
export async function Logout(user:User, database:Database) {
|
||||
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
|
|
15
webpack.config.js
Normal file
15
webpack.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const path = require('path');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
module.exports = {
|
||||
target: 'node',
|
||||
externals: [ nodeExternals() ],
|
||||
entry: './build/Binato.js',
|
||||
output: {
|
||||
path: path.join(__dirname, 'bundle'),
|
||||
filename: 'Binato.js',
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
},
|
||||
};
|
Loading…
Reference in a new issue