olgfopgkdg

This commit is contained in:
Holly Stubbs 2022-11-17 00:29:07 +00:00
parent 53a12461ce
commit 5ed106b7d4
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
15 changed files with 3123 additions and 112 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
node_modules/
build/
bundle/
tHMM.ds
server-stats.log
config.json

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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
View 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
View 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);
}
}

View file

@ -0,0 +1,3 @@
export abstract class MultiplayerManager {
}

View file

@ -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");
}

View file

@ -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);

View 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]);
}
}

View file

@ -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
View 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) {
}

View file

@ -1,6 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es6",
"esModuleInterop": true,
"resolveJsonModule": true,

15
webpack.config.js Normal file
View 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,
},
};