commit 65f47d067299912600c809690ad2897c1ba88146 Author: tgpethan Date: Thu Aug 27 13:09:35 2020 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..195a452 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +server/DatabaseHelper.js \ No newline at end of file diff --git a/Binato.js b/Binato.js new file mode 100644 index 0000000..ccdc13f --- /dev/null +++ b/Binato.js @@ -0,0 +1,49 @@ +console.clear(); + +const app = require("express")(), + fs = require("fs"), + busboy = require("connect-busboy"), + osu = require("osu-packet"); + +const debugMode = true; + +global.consoleHelper = require("./consoleHelper.js"); + +const serverHandler = require("./server/serverHandler.js"), + userManager = require("./server/userManager.js"); + +app.use(busboy()); + +app.use((req, res) => { + req.packet = new Buffer.alloc(0); + req.on("data", (chunk) => req.packet = Buffer.concat([req.packet, chunk], req.packet.length + chunk.length)); + req.on("end", () => { + switch (req.method) { + case "GET": + fs.readFile("serverPage.html", (err, data) => { + if (err) throw err; + + if (debugMode) data = data.toString().replace("|isdebug?|", 'DEBUG'); + else data = data.toString().replace("|isdebug?|", ''); + res.send(data); + }); + 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"].split(".")[0][0] == "c") + serverHandler(req, res); + break; + + default: + res.status(405).send("405 | Method not allowed!
Binato"); + break; + } + }); +}); + +// TODO: Not have a predefined port, +// doesn't matter for me so not top priority +app.listen(5001, () => global.consoleHelper.printBancho("Binato is up! Listening at port 5001")); \ No newline at end of file diff --git a/consoleHelper.js b/consoleHelper.js new file mode 100644 index 0000000..905df00 --- /dev/null +++ b/consoleHelper.js @@ -0,0 +1,33 @@ +const chalk = require("chalk"); + +module.exports = { + printWebReq:function(s) { + console.log(`${chalk.green("["+this.getTime()+"]")} ${chalk.bgGreen((" WEBREQ "))} ${s}`); + }, + + printBancho:function(s) { + console.log(`${chalk.green("["+this.getTime()+"]")} ${chalk.bgMagenta((" BANCHO "))} ${s}`); + }, + + printChat:function(s) { + console.log(`${chalk.green("["+this.getTime()+"]")} ${chalk.bgCyan((" CHAT "))} ${s}`); + }, + + printWarn:function(s) { + console.warn(`${chalk.green("["+this.getTime()+"]")} ${chalk.bgYellow((" WARN "))} ${chalk.yellow(s)}`); + }, + + printError:function(s) { + console.error(`${chalk.green("["+this.getTime()+"]")} ${chalk.bgRed((" ERROR "))} ${chalk.red(s)}`); + }, + + getTime:function() { + const time = new Date(); + return `${correctValue(time.getHours())}:${correctValue(time.getMinutes())}:${correctValue(time.getSeconds())}`; + } +} + +function correctValue(i) { + if (i <= 9) return "0"+i; + else return i; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6c9f39b --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "Binato", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "connect-busboy": "0.0.2", + "express": "^4.17.1", + "fs": "0.0.1-security", + "osu-packet": "^4.1.2", + "request": "^2.88.2", + "sync-request": "^6.1.0", + "uuid": "^7.0.3" + } +} diff --git a/server/BotCommandHandler.js b/server/BotCommandHandler.js new file mode 100644 index 0000000..db3560b --- /dev/null +++ b/server/BotCommandHandler.js @@ -0,0 +1,13 @@ +module.exports = function(User, Message) { + const command = Message.split(" ")[0]; + + switch (command) { + case "!help": + + break; + + case "!lock": + + break; + } +} \ No newline at end of file diff --git a/server/Multiplayer.js b/server/Multiplayer.js new file mode 100644 index 0000000..2936415 --- /dev/null +++ b/server/Multiplayer.js @@ -0,0 +1,528 @@ +const osu = require("osu-packet"), + getUserById = require("./util/getUserById.js"), + DatabaseHelper = require("./DatabaseHelper.js"), + StatusUpdate = require("./Packets/StatusUpdate.js"); + +module.exports = { + userEnterLobby:function(currentUser) { + if (currentUser.currentMatch != null) { + this.leaveMatch(currentUser); + currentUser.currentMatch = null; + } + + global.StreamsHandler.addUserToStream("multiplayer_lobby", currentUser.id); + + const osuPacketWriter1 = new osu.Bancho.Writer; + let userIds = []; + + for (let i = 0; i < global.users.length; i++) { + userIds.push(global.users[i].id); + } + + osuPacketWriter1.UserPresenceBundle(userIds); + + global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter1.toBuffer, null); + + for (let i = 0; i < global.matches.length; i++) { + for (let i1 = 0; i1 < global.matches[i][1].slots.length; i1++) { + const slot = global.matches[i][1].slots[i1]; + if (slot.playerId == -1 || slot.status == 2) continue; + const osuPacketWriter = new osu.Bancho.Writer; + + const User = getUserById(slot.playerId); + + // Get user score info from the database + const userScoreDB = DatabaseHelper.getFromDB(`SELECT * FROM users_modes_info WHERE user_id = ${User.id} AND mode_id = ${User.playMode} LIMIT 1`); + + let UserStatusObject = { + userId: User.id, + status: User.actionID, + statusText: User.actionText, + beatmapChecksum: User.beatmapChecksum, + currentMods: User.currentMods, + playMode: User.playMode, + beatmapId: User.beatmapID, + rankedScore: userScoreDB.ranked_score, + accuracy: userScoreDB.avg_accuracy / 100, // Scale of 0 to 1 + playCount: userScoreDB.playcount, + totalScore: userScoreDB.total_score, + rank: 1, + performance: userScoreDB.pp_raw + }; + + osuPacketWriter.HandleOsuUpdate(UserStatusObject); + + global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null); + } + const osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.MatchNew(global.matches[i][1]); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + } + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.ChannelJoinSuccess("#lobby"); + if (!global.StreamsHandler.isUserInStream("#lobby", currentUser.id)) + global.StreamsHandler.addUserToStream("#lobby", currentUser.id); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + }, + + updateMatchListing:function() { + const osuPacketWriter1 = new osu.Bancho.Writer; + let userIds = []; + + for (let i = 0; i < global.users.length; i++) { + userIds.push(global.users[i].id); + } + + osuPacketWriter1.UserPresenceBundle(userIds); + + global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter1.toBuffer, null); + for (let i = 0; i < global.matches.length; i++) { + for (let i1 = 0; i1 < global.matches[i][1].slots.length; i1++) { + const slot = global.matches[i][1].slots[i1]; + if (slot.playerId == -1 || slot.status == 2) continue; + const osuPacketWriter = new osu.Bancho.Writer; + + const User = getUserById(slot.playerId); + + // Get user score info from the database + const userScoreDB = DatabaseHelper.getFromDB(`SELECT * FROM users_modes_info WHERE user_id = ${User.id} AND mode_id = ${User.playMode} LIMIT 1`); + + let UserStatusObject = { + userId: User.id, + status: User.actionID, + statusText: User.actionText, + beatmapChecksum: User.beatmapChecksum, + currentMods: User.currentMods, + playMode: User.playMode, + beatmapId: User.beatmapID, + rankedScore: userScoreDB.ranked_score, + accuracy: userScoreDB.avg_accuracy / 100, // Scale of 0 to 1 + playCount: userScoreDB.playcount, + totalScore: userScoreDB.total_score, + rank: 1, + performance: userScoreDB.pp_raw + }; + + osuPacketWriter.HandleOsuUpdate(UserStatusObject); + + global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null); + } + const osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.MatchNew(global.matches[i][1]); + + global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null); + } + }, + + createMultiplayerMatch:function(currentUser, data) { + const osuPacketWriter = new osu.Bancho.Writer; + + if (data.gamePassword == '') data.gamePassword == null; + + let NewMatchObject = { + matchId: global.matches.length, + inProgress: false, + matchType: 0, + activeMods: 0, + gameName: data.gameName, + gamePassword: data.gamePassword, + beatmapName: data.beatmapName, + beatmapId: data.beatmapId, + beatmapChecksum: data.beatmapChecksum, + slots: data.slots, + host: currentUser.id, + playMode: 0, + matchScoringType: 0, + matchTeamType: 0, + specialModes: 0, + hidden: false, + seed: data.seed + } + + for (let i = 0; i < NewMatchObject.slots.length; i++) { + let s = NewMatchObject.slots[i]; + s.mods = 0; + } + + StatusUpdate(currentUser, currentUser.id); + osuPacketWriter.MatchNew(NewMatchObject); + + // Queue match creation for user + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + + global.StreamsHandler.addStream(`mp_${data.gameName.split(" ").join("-")}`, true, NewMatchObject.matchId); + + global.matches.push([`mp_${data.gameName.split(" ").join("-")}`, NewMatchObject]); + + this.updateMatchListing(); + + this.joinMultiplayerMatch(currentUser, { + matchId: NewMatchObject.matchId, + gamePassword: NewMatchObject.gamePassword + }); + }, + + joinMultiplayerMatch:function(currentUser, data) { + try { + let osuPacketWriter = new osu.Bancho.Writer; + const osuPacketWriter1 = new osu.Bancho.Writer; + + const streamName = global.matches[data.matchId][0]; + const mpLobby = global.matches[data.matchId][1]; + + let full = true; + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId !== -1 || slot.status === 2) continue; + full = false; + slot.playerId = currentUser.id; + currentUser.matchSlotId = i; + slot.status = 4; + break; + } + + osuPacketWriter1.MatchUpdate(mpLobby); + osuPacketWriter.MatchJoinSuccess(mpLobby); + + if (full) { + osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.MatchJoinFail(); + } else { + global.StreamsHandler.removeUserFromStream("multiplayer_lobby", currentUser.id); + } + + currentUser.currentMatch = data.matchId; + + global.StreamsHandler.addUserToStream(streamName, currentUser.id); + + global.StreamsHandler.sendToStream(streamName, osuPacketWriter1.toBuffer, null); + + osuPacketWriter.ChannelJoinSuccess("#multiplayer"); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + } catch (e) { + const osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.MatchJoinFail(); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + + this.updateMatchListing(); + } + }, + + setReadyState:function(currentUser, state) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + const osuPacketWriter = new osu.Bancho.Writer; + + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId == currentUser.id) { + if (state) slot.status = 8; + else slot.status = 4; + console.log("e"); + break; + } + } + + osuPacketWriter.MatchUpdate(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + sendMatchUpdate:function(currentUser) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + const osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.MatchUpdate(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + updateMatch:function(currentUser, data) { + global.matches[currentUser.currentMatch][1] = data; + const osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.MatchUpdate(global.matches[currentUser.currentMatch][1]); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + moveToSlot:function(currentUser, data) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + const osuPacketWriter = new osu.Bancho.Writer; + + let currentUserData, slotIndex; + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId != currentUser.id) continue; + + currentUserData = slot; + slotIndex = i; + break; + } + + mpLobby.slots[data].playerId = currentUserData.playerId; + currentUser.matchSlotId = data; + mpLobby.slots[data].status = currentUserData.status; + + mpLobby.slots[slotIndex].playerId = -1; + mpLobby.slots[slotIndex].status = 1; + + osuPacketWriter.MatchUpdate(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + kickPlayer:function(currentUser, data) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + const osuPacketWriter = new osu.Bancho.Writer; + + if (mpLobby.host != currentUser.id) return; + + const slot = mpLobby.slots[data]; + let cachedPlayerId = slot.playerId; + + if (slot.playerId === -1) { // Slot is empty, lock it + if (slot.status === 1) slot.status = 2; + else slot.status = 1; + } else { // Slot isn't empty kick player + const kickedPlayer = getUserById(slot.playerId); + kickedPlayer.matchSlotId = -1; + slot.playerId = -1; + slot.status = 1; + } + + osuPacketWriter.MatchUpdate(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + + if (cachedPlayerId !== null || cachedPlayerId !== -1) { + global.StreamsHandler.removeUserFromStream(global.matches[currentUser.currentMatch][0], cachedPlayerId); + } + }, + + missingBeatmap:function(currentUser, state) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + const osuPacketWriter = new osu.Bancho.Writer; + + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId != currentUser.id) continue; + + if (state) { + slot.status = 16; + } else { + slot.status = 4; + } + break; + } + + osuPacketWriter.MatchUpdate(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + transferHost:function(currentUser, data) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + const osuPacketWriter = new osu.Bancho.Writer; + + const newUser = getUserById(mpLobby.slots[data].playerId); + + mpLobby.host = newUser.id; + + osuPacketWriter.MatchUpdate(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + updateMods(currentUser, data) { // TODO: Allow freemod to work + if (global.matches[currentUser.currentMatch][1].host !== currentUser.id) return; + const osuPacketWriter = new osu.Bancho.Writer; + + global.matches[currentUser.currentMatch][1].activeMods = data; + + osuPacketWriter.MatchUpdate(global.matches[currentUser.currentMatch][1]); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + startMatch(currentUser) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + if (mpLobby.inProgress) return; + mpLobby.inProgress = true; + global.matches[currentUser.currentMatch][2] = []; + const loadedSlots = global.matches[currentUser.currentMatch][2]; + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; + + loadedSlots.push({playerId: slot.playerId, loaded: false}); + } + const osuPacketWriter = new osu.Bancho.Writer; + + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; + + slot.status = 32; + } + + osuPacketWriter.MatchStart(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + + this.sendMatchUpdate(currentUser); + }, + + setPlayerLoaded:function(currentUser) { + const loadedSlots = global.matches[currentUser.currentMatch][2]; + + for (let i = 0; i < loadedSlots.length; i++) { + if (loadedSlots[i].playerId == currentUser.id) { + loadedSlots[i].loaded = true; + } + } + + let allLoaded = true; + for (let i = 0; i < loadedSlots.length; i++) { + if (loadedSlots[i].loaded) continue; + + allLoaded = false; + } + + if (allLoaded) { + let osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.MatchAllPlayersLoaded(); + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + + global.matches[currentUser.currentMatch][2] = null; + } + }, + + onPlayerFinishMatch:function(currentUser) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + if (global.matches[currentUser.currentMatch][2] == null) { + global.matches[currentUser.currentMatch][2] = []; + const loadedSlots = global.matches[currentUser.currentMatch][2]; + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; + + loadedSlots.push({playerId: slot.playerId, loaded: false}); + } + } + + const loadedSlots = global.matches[currentUser.currentMatch][2]; + + for (let i = 0; i < loadedSlots.length; i++) { + if (loadedSlots[i].playerId == currentUser.id) { + loadedSlots[i].loaded = true; + } + } + + let allLoaded = true; + for (let i = 0; i < loadedSlots.length; i++) { + if (loadedSlots[i].loaded) continue; + + allLoaded = false; + } + + if (allLoaded) this.finishMatch(currentUser); + }, + + finishMatch:function(currentUser) { + const mpLobby = global.matches[currentUser.currentMatch][1]; + if (!mpLobby.inProgress) return; + global.matches[currentUser.currentMatch][2] = []; + mpLobby.inProgress = false; + let osuPacketWriter = new osu.Bancho.Writer; + + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; + + slot.status = 4; + } + + osuPacketWriter.MatchComplete(); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + + this.sendMatchUpdate(currentUser); + }, + + updatePlayerScore:function(currentUser, data) { + const osuPacketWriter = new osu.Bancho.Writer; + + if (currentUser.matchSlotId == -1) return console.log("it did the big fuck"); + + data.id = currentUser.matchSlotId; + + osuPacketWriter.MatchScoreUpdate(data); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + }, + + leaveMatch:function(currentUser) { + try { + const mpLobby = global.matches[currentUser.currentMatch][1]; + let osuPacketWriter = new osu.Bancho.Writer; + + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId != currentUser.id) continue; + + slot.playerId = -1; + slot.status = 1; + + break; + } + + osuPacketWriter.MatchUpdate(mpLobby); + + global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); + + osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.ChannelRevoked("#multiplayer"); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + + let empty = true; + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + if (slot.playerId === -1) continue; + + empty = false; + break; + } + + if (empty) { + let matchIndex; + for (let i = 0; i < global.matches.length; i++) { + if (global.matches[i][0] == global.matches[currentUser.currentMatch][0]) { + matchIndex = i; + break; + } + } + + // Make sure we got a match index + if (matchIndex == null) return; + + global.matches.splice(matchIndex, 1); + + + } + } catch (e) { } + this.updateMatchListing(); + + setTimeout(() => { + this.updateMatchListing(); + }, 500); + } +} \ No newline at end of file diff --git a/server/Packets/ChangeAction.js b/server/Packets/ChangeAction.js new file mode 100644 index 0000000..5fcb6a6 --- /dev/null +++ b/server/Packets/ChangeAction.js @@ -0,0 +1,3 @@ +module.exports = function(currentUser, data) { + currentUser.updatePresence(data); +} \ No newline at end of file diff --git a/server/Packets/ChannelPart.js b/server/Packets/ChannelPart.js new file mode 100644 index 0000000..4703e78 --- /dev/null +++ b/server/Packets/ChannelPart.js @@ -0,0 +1,3 @@ +module.exports = function(userClass, data) { + global.StreamsHandler.removeUserFromStream(data, userClass.id); +} \ No newline at end of file diff --git a/server/Packets/Logout.js b/server/Packets/Logout.js new file mode 100644 index 0000000..da78a5c --- /dev/null +++ b/server/Packets/Logout.js @@ -0,0 +1,35 @@ +const osu = require("osu-packet"); + +module.exports = function(CurrentUser) { + const loginStartTime = new Date().getTime(); + let userCurrentIndex; + // Find the index that the user's class is at + for (let i = 0; i < global.users.length; i++) { + if (CurrentUser.uuid == global.users[i].uuid) { + userCurrentIndex = i; + break; + } + } + + const streamList = global.StreamsHandler.getStreams(); + + for (let i = 0; i < streamList.length; i++) { + if (global.StreamsHandler.isUserInStream(streamList[i], CurrentUser.id)) { + global.StreamsHandler.removeUserFromStream(streamList[i], CurrentUser.id); + } + } + + // Remove that user from the list of users + global.users.splice(userCurrentIndex, 1); + + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.SendMessage({ + sendingClient: "BanchoBot", + message: `User ${CurrentUser.username} has logged out.`, + target: "#userlog", + senderId: 3 + }); + global.StreamsHandler.sendToStream("#userlog", osuPacketWriter.toBuffer); + + global.consoleHelper.printBancho(`User logged out, took ${new Date().getTime() - loginStartTime}ms. [User: ${CurrentUser.username}]`); +} \ No newline at end of file diff --git a/server/Packets/SendPublicMessage.js b/server/Packets/SendPublicMessage.js new file mode 100644 index 0000000..4b0ebf2 --- /dev/null +++ b/server/Packets/SendPublicMessage.js @@ -0,0 +1,21 @@ +const osu = require("osu-packet"), + userManager = require("../userManager.js"); + +module.exports = function(CurrentPacket, CurrentUser) { + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.SendMessage({ + sendingClient: CurrentUser.username, + message: CurrentPacket.data.message, + target: CurrentPacket.data.target, + senderId: CurrentUser.id + }); + + if (CurrentPacket.data.target == "#multiplayer") + return global.StreamsHandler.sendToStream(global.matches[CurrentUser.currentMatch][0], osuPacketWriter.toBuffer, CurrentUser.id); + + // Check the stream that we're sending to even exists + if (!global.StreamsHandler.doesStreamExist(CurrentPacket.data.target)) return; + + // Write chat message to stream asociated with chat channel + return global.StreamsHandler.sendToStream(CurrentPacket.data.target, osuPacketWriter.toBuffer, CurrentUser.id); +} \ No newline at end of file diff --git a/server/Packets/StatusUpdate.js b/server/Packets/StatusUpdate.js new file mode 100644 index 0000000..06b013f --- /dev/null +++ b/server/Packets/StatusUpdate.js @@ -0,0 +1,35 @@ +const osu = require("osu-packet"), + getUserById = require("../util/getUserById.js"); + +module.exports = function(currentUser, id) { + if (id == 3) return; // Ignore Bot + + // Create new osu packet writer + const osuPacketWriter = new osu.Bancho.Writer; + + // Get user's class + const User = getUserById(id); + + if (User == null) return; + + let UserStatusObject = { + userId: User.id, + status: User.actionID, + statusText: User.actionText, + beatmapChecksum: User.beatmapChecksum, + currentMods: User.currentMods, + playMode: User.playMode, + beatmapId: User.beatmapID, + rankedScore: User.rankedScore, + accuracy: User.accuracy / 100, // Scale of 0 to 1 + playCount: User.playCount, + totalScore: User.totalScore, + rank: User.rank, + performance: User.pp + }; + + osuPacketWriter.HandleOsuUpdate(UserStatusObject); + + // Send data to user's queue + currentUser.addActionToQueue(osuPacketWriter.toBuffer); +} \ No newline at end of file diff --git a/server/Packets/UserPresence.js b/server/Packets/UserPresence.js new file mode 100644 index 0000000..f522efc --- /dev/null +++ b/server/Packets/UserPresence.js @@ -0,0 +1,27 @@ +const osu = require("osu-packet"), + getUserById = require("../util/getUserById.js"); + +module.exports = function(currentUser, id) { + if (id == 3) return; // Bot + + const osuPacketWriter = new osu.Bancho.Writer; + + const User = getUserById(id); + + if (User == null) return; + + let UserPresenceObject = { + userId: id, + username: User.username, + timezone: 0, + countryId: User.countryID, + permissions: 4, + longitude: User.location[1], + latitude: User.location[0], + rank: User.rank + }; + + osuPacketWriter.UserPresence(UserPresenceObject); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); +} \ No newline at end of file diff --git a/server/Packets/UserPresenceBundle.js b/server/Packets/UserPresenceBundle.js new file mode 100644 index 0000000..ef74fed --- /dev/null +++ b/server/Packets/UserPresenceBundle.js @@ -0,0 +1,16 @@ +const osu = require("osu-packet"), + userManager = require("../userManager.js"); + +module.exports = function(currentUser) { + const osuPacketWriter = new osu.Bancho.Writer; + + let userIds = []; + + for (let i = 0; i < global.users.length; i++) { + userIds.push(global.users[i].id); + } + + osuPacketWriter.UserPresenceBundle(userIds); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); +} \ No newline at end of file diff --git a/server/Packets/UserStatsRequest.js b/server/Packets/UserStatsRequest.js new file mode 100644 index 0000000..532e34f --- /dev/null +++ b/server/Packets/UserStatsRequest.js @@ -0,0 +1,14 @@ +const UserPresenceBundle = require("./UserPresenceBundle.js"), + UserPresence = require("./UserPresence.js"), + StatusUpdate = require("./StatusUpdate.js"); + +module.exports = function (currentUser, data) { + UserPresenceBundle(currentUser); + + for (let i1 = 0; i1 < data.length; i1++) { + const CurrentUserID = data[i1]; + + UserPresence(currentUser, CurrentUserID); + StatusUpdate(currentUser, CurrentUserID); + } +} \ No newline at end of file diff --git a/server/Spectator.js b/server/Spectator.js new file mode 100644 index 0000000..1024b06 --- /dev/null +++ b/server/Spectator.js @@ -0,0 +1,75 @@ +const osu = require("osu-packet"), + getUserById = require("./util/getUserById.js"); + +module.exports = { + startSpectatingUser:function(spectatedId, currentUser) { + // Get the user this user is trying to spectate + const User = getUserById(spectatedId); + if (global.StreamsHandler.doesStreamExist(`sp_${User.username}`)) { + // Just add user to stream since it already exists + global.StreamsHandler.addUserToStream(`sp_${User.username}`, currentUser.id); + } else { + // Stream doesn't exist, create it and add the spectator + global.StreamsHandler.addStream(`sp_${User.username}`, true, spectatedId); + global.StreamsHandler.addUserToStream(`sp_${User.username}`, currentUser.id); + } + + // We want to do this stuff regardless + // Create a new osu packet writer + let osuPacketWriter = new osu.Bancho.Writer; + + // Set the user requesting to be spectating this user + currentUser.spectating = spectatedId; + + // Tell the client of the user being spectated that they are being spectated + osuPacketWriter.SpectatorJoined(currentUser.id); + + // Send the packet to the spectated user's queue + User.addActionToQueue(osuPacketWriter.toBuffer); + + // Make a new clear osu packet writer + osuPacketWriter = new osu.Bancho.Writer; + + // Tell everyone spectating this user that another user has started spectating + osuPacketWriter.FellowSpectatorJoined(currentUser.id); + + // Send this packet to all the spectators + global.StreamsHandler.sendToStream(`sp_${User.username}`, osuPacketWriter.toBuffer); + }, + + sendSpectatorFrames(currentUser, data) { + // Create new osu packet writer + const osuPacketWriter = new osu.Bancho.Writer; + + // Data containing the user's actions + osuPacketWriter.SpectateFrames(data); + + // Send the frames to all the spectators + global.StreamsHandler.sendToStream(`sp_${currentUser.username}`, osuPacketWriter.toBuffer, null); + }, + + stopSpectatingUser(currentUser) { + // Get the user this user is spectating + const spectatedUser = getUserById(currentUser.spectating); + // Create new osu packet writer + let osuPacketWriter = new osu.Bancho.Writer; + + // Inform the client being spectated that this user has stopped spectating + osuPacketWriter.SpectatorLeft(currentUser.id); + + // Add this packet to the spectated user's queue + spectatedUser.addActionToQueue(osuPacketWriter.toBuffer); + + // Remove this user from the spectator stream + global.StreamsHandler.removeUserFromStream(`sp_${spectatedUser.username}`, currentUser.id); + + // Make a new clear osu packet writer + osuPacketWriter = new osu.Bancho.Writer; + + // Inform other users spectating that this spectator has left + osuPacketWriter.FellowSpectatorLeft(currentUser.id); + + // Send this packet to all spectators + global.StreamsHandler.sendToStream(`sp_${spectatedUser.username}`, osuPacketWriter.toBuffer); + } +} \ No newline at end of file diff --git a/server/Streams.js b/server/Streams.js new file mode 100644 index 0000000..12aada5 --- /dev/null +++ b/server/Streams.js @@ -0,0 +1,115 @@ +const getUserById = require("./util/getUserById.js"); + +module.exports = class { + constructor() { + this.avaliableStreams = {}; + } + + addStream(streamName, removeIfEmpty, spectatorHostId = null) { + // Add new stream to the list of streams + this.avaliableStreams[streamName] = { + streamUsers: [], // An array containing a list of user IDs of the users in a given stream + streamSpectatorHost: spectatorHostId, // null unless stream is for spectating + removeIfEmpty: removeIfEmpty + } + global.consoleHelper.printBancho(`Added stream [${streamName}]`); + } + + // Checks if a stream has no users in it + streamChecker(interval) { + setInterval(() => { + // Get the names of all currently avaliable streams + const streams = global.StreamsHandler.getStreams(); + // Loop through all streams + for (let i = 0; i < streams.length; i++) { + // Get the current stream + const currentStream = global.StreamsHandler.avaliableStreams[streams[i]]; + // Check if the stream should be removed if there are no users in it + // And if the stream has no users in it + if (currentStream.removeIfEmpty && currentStream.streamUsers.length == 0) { + global.StreamsHandler.removeStream(streams[i]); + global.consoleHelper.printBancho(`Removed stream [${streams[i]}] There were no users in stream`); + } + } + }, interval); + global.consoleHelper.printBancho(`BinatoStream is running! Checks running at a ${interval}ms interval`); + } + + sendToStream(streamName, streamData, initUser = null) { + // Get the stream to send the data to + const currentStream = this.avaliableStreams[streamName]; + + try { + // Loop through the users in this stream + for (let i = 0; i < currentStream.streamUsers.length; i++) { + // Get the user id of the user in the queue + const currentUserId = currentStream.streamUsers[i]; + // Make sure we don't send this data back to the user requesting this data to be sent + if (initUser != null && currentUserId == initUser && (streamName[0] == "#" || streamName.includes("mp_"))) continue; + if (currentUserId == 3) continue; // Skip if user is bot + + // Get user object + const currentUser = getUserById(currentUserId); + // Skip if user is nonexistant + if (currentUser == null) continue; + + // Send stream data to user's own queue + currentUser.addActionToQueue(streamData); + } + } catch (e) {} + } + + addUserToStream(streamName, userId) { + // Add user's id to the stream's user list + this.avaliableStreams[streamName].streamUsers.push(userId); + global.consoleHelper.printBancho(`Added user [${userId}] to stream ${streamName}`); + } + + removeUserFromStream(streamName, userId) { + // Make sure this isn't an invalid user + if (userId == -1 || userId == null) return; + try { + // Find index of user to remove + let userCurrentIndex; + for (let i = 0; i < this.avaliableStreams[streamName].streamUsers.length; i++) { + if (userId == this.avaliableStreams[streamName].streamUsers[i]) { + userCurrentIndex = i; + break; + } + } + + // Remove user from stream's user list + this.avaliableStreams[streamName].streamUsers.splice(userCurrentIndex, 1); + global.consoleHelper.printBancho(`Removed user [${userId}] from stream ${streamName}`); + } catch (e) { global.consoleHelper.printBancho(`Can't Remove user [${userId}] from stream ${streamName}`); } + } + + doesStreamExist(streamName) { + let exist = false; + const streamList = Object.keys(this.avaliableStreams); + for (let i = 0; i < streamList.length; i++) { + if (streamList[i] == streamName) { + exist = true; + break; + } + } + + return exist; + } + + getStreams() { + // Return the names of all avaliable streams + return Object.keys(this.avaliableStreams); + } + + isUserInStream(streamName, userId) { + if (this.avaliableStreams[streamName].streamUsers.includes(userId)) return true; + else return false; + } + + removeStream(streamName) { + try { + delete this.avaliableStreams[streamName]; + } catch (e) { global.consoleHelper.printError(`Was not able to remove stream [${streamName}]`) } + } +} \ No newline at end of file diff --git a/server/User.js b/server/User.js new file mode 100644 index 0000000..178c9de --- /dev/null +++ b/server/User.js @@ -0,0 +1,72 @@ +const DatabaseHelper = require("./DatabaseHelper.js"); + +module.exports = class { + constructor(id, username, uuid, connectTime) { + this.id = id; + this.username = username; + this.uuid = uuid; + this.connectTime = connectTime; + this.queue = new Buffer.alloc(0); + + this.playMode = 0; + this.countryID = 0; + this.spectators = []; + this.spectating = 0; + this.location = [0,0]; + this.joinedChannels = []; + this.currentMatch = null; + this.matchSlotId = -1; + + // Presence data + this.actionID = 0; + this.actionText = ""; + this.actionMods = 0; + this.beatmapChecksum = ""; + this.beatmapID = 0; + this.currentMods = 0; + + // Cached db data + this.rankedScore = 0; + this.accuracy = 0; + this.playCount = 0; + this.totalScore = 0; + this.rank = 0; + this.pp = 0; + } + + // Adds new actions to the user's queue + addActionToQueue(newData) { + this.queue = Buffer.concat([this.queue, newData], this.queue.length + newData.length); + } + + // Updates the user's current action + updatePresence(action) { + this.actionID = action.status; + this.actionText = action.statusText; + this.beatmapChecksum = action.beatmapChecksum; + this.currentMods = action.currentMods; + this.actionMods = action.currentMods; + this.playMode = action.playMode; + this.beatmapID = action.beatmapId; + } + + // Gets the user's score information from the database and caches it + getNewUserInformationFromDatabase() { + const userScoreDB = DatabaseHelper.getFromDB(`SELECT * FROM users_modes_info WHERE user_id = ${this.id} AND mode_id = ${this.playMode} LIMIT 1`); + const userRankDB = DatabaseHelper.getFromDB(`SELECT user_id, pp_raw, FIND_IN_SET( pp_raw, ( SELECT GROUP_CONCAT( pp_raw ORDER BY pp_raw DESC ) FROM users_modes_info WHERE mode_id = ${this.playMode} ) ) AS rank FROM users_modes_info WHERE user_id = ${this.id} AND mode_id = ${this.playMode}`); + + if (userScoreDB == null || userRankDB == null) throw "fuck"; + + this.rankedScore = userScoreDB.ranked_score; + this.totalScore = userScoreDB.total_score; + this.accuracy = userScoreDB.avg_accuracy; + this.playCount = userScoreDB.playcount; + this.rank = userRankDB.rank; + this.pp = userScoreDB.pp_raw; + } + + // Clears out the user's queue + clearQueue() { + this.queue = new Buffer.alloc(0); + } +} \ No newline at end of file diff --git a/server/bakedResponses.js b/server/bakedResponses.js new file mode 100644 index 0000000..9e58b15 --- /dev/null +++ b/server/bakedResponses.js @@ -0,0 +1,9 @@ +module.exports = function(s) { + switch (s) { + case "reconnect": + return "\u0005\u0000\u0000\u0004\u0000\u0000\u0000����\u0018\u0000\u0000\u0011\u0000\u0000\u0000\u000b\u000fReconnecting..."; + + default: + return Buffer.alloc(0); + } +} \ No newline at end of file diff --git a/server/countryHelper.js b/server/countryHelper.js new file mode 100644 index 0000000..27a124f --- /dev/null +++ b/server/countryHelper.js @@ -0,0 +1,271 @@ +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 +} + +module.exports.countryCodes = countryCodes; + +module.exports = { + getCountryID:function(code) { + // Get id of a country from a 2 char code + const s = `${code}`.toUpperCase(); + if (countryCodes[s] != null) return countryCodes[s]; + else return 0; + }, + + getCountryLetters:function(code) { + // Get country char code from id + for (var i = 0; i < countryCodes.length; i++) { + if (countryCodes[i] === code) return countryCodes[i]; + } + return "XX"; + } +} \ No newline at end of file diff --git a/server/loginHandler.js b/server/loginHandler.js new file mode 100644 index 0000000..30d9bd5 --- /dev/null +++ b/server/loginHandler.js @@ -0,0 +1,159 @@ +const osu = require("osu-packet"), + User = require("./User.js"), + { v4: uuid } = require('uuid'), + request = require("sync-request"), + + userManager = require("./userManager.js"), + DatabaseHelper = require("./DatabaseHelper.js"), + countryHelper = require("./countryHelper.js"), + loginHelper = require("./loginHelper.js"); + +module.exports = function(req, res, loginInfo) { + // Get time at the start of login + const loginStartTime = new Date().getTime(); + + // Check login + const loginCheck = loginHelper.checkLogin(loginInfo); + if (loginCheck != null) { + res.writeHead(200, loginCheck[1]); + return res.end(loginCheck[0]); + } + + // Get users IP for getting location + let requestIP = req.get('X-Real-IP'); + if (requestIP == null) { + requestIP = req.remote_addr; + } + + let userLocationData = [], userLocation; + // Check if it is a local IP + if (`${requestIP}`.includes("192.168.")) { + userLocationData.country = "GB"; // My country + userLocation = [53, -2]; // My rough location lol + } else { + // Get user's location from zxq + userLocationData = JSON.parse(request("GET", `http://ip.zxq.co/${requestIP}`).getBody()); + userLocation = userLocationData.loc.split(","); + } + + // Get information about the user from the database + const userDB = DatabaseHelper.getFromDB(`SELECT id FROM users_info WHERE username = "${loginInfo.username}" LIMIT 1`); + + // Create a token for the client + const newClientToken = uuid(); + + // Make sure user is not already connected, kick off if so. + const checkForPreexistingUser = userManager.getUserFromUsername(loginInfo.username); + if (checkForPreexistingUser != null) { + if (checkForPreexistingUser.uuid != newClientToken) { + let userCurrentIndex; + // Find the index that the user's class is at + for (let i = 0; i < global.users.length; i++) { + if (checkForPreexistingUser.uuid == global.users[i].uuid) { + userCurrentIndex = i; + break; + } + } + + global.users.splice(userCurrentIndex, 1); + } + } + + // Create user object + global.users.push(new User(userDB.id, loginInfo.username, newClientToken, new Date().getTime())); + + // Retreive the newly created user + const userClass = userManager.getUserFromToken(newClientToken); + + // Get user's data from the database + userClass.getNewUserInformationFromDatabase(); + + try { + // Save the user's location to their class for later use + userClass.location[0] = parseFloat(userLocation[0]); + userClass.location[1] = parseFloat(userLocation[1]); + + // Save the country id for the same reason as above + userClass.countryID = countryHelper.getCountryID(userLocationData.country); + + // 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(userClass.id); + // Current bancho protocol version is 19 + osuPacketWriter.ProtocolNegotiation(19); + // Permission level 4 is osu!supporter + osuPacketWriter.LoginPermissions(4); + // Send user's friends list + // TODO: friends? Haha, you have no friends! + // Set title screen image + osuPacketWriter.TitleUpdate("http://puu.sh/jh7t7/20c04029ad.png|https://osu.ppy.sh/news/123912240253"); + + // Construct user panel data + const presenceObject = { + userId: userClass.id, + username: userClass.username, + timezone: 0, + countryId: userClass.countryID, + permissions: 4, + longitude: userClass.location[1], + latitude: userClass.location[0], + rank: userClass.rank + }; + + const statusObject = { + userId: userClass.id, + status: userClass.actionID, + statusText: userClass.actionText, + beatmapChecksum: userClass.beatmapChecksum, + currentMods: userClass.currentMods, + playMode: userClass.playMode, + beatmapId: userClass.beatmapID, + rankedScore: userClass.rankedScore, + accuracy: userClass.accuracy / 100, // Scale of 0 to 1 + playCount: userClass.playCount, + totalScore: userClass.totalScore, + rank: userClass.rank, + performance: userClass.pp + }; + + // Add user panel data packets + osuPacketWriter.UserPresence(presenceObject); + osuPacketWriter.HandleOsuUpdate(statusObject); + + // peppy pls, why + osuPacketWriter.ChannelListingComplete(); + + // Add user to chat channels + osuPacketWriter.ChannelJoinSuccess("#osu"); + if (!global.StreamsHandler.isUserInStream("#osu", userClass.id)) + global.StreamsHandler.addUserToStream("#osu", userClass.id); + + osuPacketWriter.ChannelJoinSuccess("#userlog"); + if (!global.StreamsHandler.isUserInStream("#userlog", userClass.id)) + global.StreamsHandler.addUserToStream("#userlog", userClass.id); + + // List all channels out to the client + //for (let i = 0; i < global.channels.length; i++) { + // osuPacketWriter.ChannelAvailable(global.channels[i]); + //} + + osuPacketWriter.Announce(`Welcome back ${loginInfo.username}!`); + + // Complete login + res.writeHead(200, { + "cho-token": userClass.uuid, + "cho-protocol": 19, + "Connection": "keep-alive", + "Keep-Alive": "timeout=5, max=100", + "Content-Type": "text/html; charset=UTF-8" + }); + res.end(osuPacketWriter.toBuffer, () => { + global.consoleHelper.printBancho(`User login finished, took ${new Date().getTime() - loginStartTime}ms. [User: ${loginInfo.username}]`); + }); + } catch (err) { + console.error(err); + } +} \ No newline at end of file diff --git a/server/loginHelper.js b/server/loginHelper.js new file mode 100644 index 0000000..b8ff0f7 --- /dev/null +++ b/server/loginHelper.js @@ -0,0 +1,37 @@ +const DatabaseHelper = require("./DatabaseHelper.js"), + osu = require("osu-packet"); + +module.exports = { + checkLogin:function(loginInfo) { + // Queue up incorrect login response + const incorrectDetailsResponse = incorrectLoginResponse(); + // Check if there is any login information provided + if (loginInfo == null) return incorrectDetailsResponse; + + const userDBData = DatabaseHelper.getFromDB(`SELECT * FROM users_info WHERE username = "${loginInfo.username}" LIMIT 1`); + + // Make sure a user was found in the database + if (Object.keys(userDBData).length < 1) return incorrectDetailsResponse; + // Make sure the username is the same as the login info + if (userDBData.username !== loginInfo.username) return incorrectDetailsResponse; + // Make sure the password is the same as the login info + if (userDBData.password !== loginInfo.password) return incorrectDetailsResponse; + + return null; + } +} + +function incorrectLoginResponse() { + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.LoginReply(-1); + return [ + osuPacketWriter.toBuffer, + { + 'cho-token': 'No', + 'cho-protocol': 19, + 'Connection': 'keep-alive', + 'Keep-Alive': 'timeout=5, max=100', + 'Content-Type': 'text/html; charset=UTF-8' + } + ]; +} \ No newline at end of file diff --git a/server/packetIDs.json b/server/packetIDs.json new file mode 100644 index 0000000..116489f --- /dev/null +++ b/server/packetIDs.json @@ -0,0 +1,111 @@ +{ + "client_changeAction":0, + "client_sendPublicMessage":1, + "client_logout":2, + "client_requestStatusUpdate":3, + "client_pong":4, + "server_userID":5, + "server_commandError":6, + "server_sendMessage":7, + "server_ping":8, + "server_handleIRCUsernameChange":9, + "server_handleIRCQuit":10, + "server_userStats":11, + "server_userLogout":12, + "server_spectatorJoined":13, + "server_spectatorLeft":14, + "server_spectateFrames":15, + "client_startSpectating":16, + "client_stopSpectating":17, + "client_spectateFrames":18, + "server_versionUpdate":19, + "client_errorReport":20, + "client_cantSpectate":21, + "server_spectatorCantSpectate":22, + "server_getAttention":23, + "server_notification":24, + "client_sendPrivateMessage":25, + "server_updateMatch":26, + "server_newMatch":27, + "server_disposeMatch":28, + "client_partLobby":29, + "client_joinLobby":30, + "client_createMatch":31, + "client_joinMatch":32, + "client_partMatch":33, + "server_lobbyJoin_obsolete":34, + "server_lobbyPart_obsolete":35, + "server_matchJoinSuccess":36, + "server_matchJoinFail":37, + "client_matchChangeSlot":38, + "client_matchReady":39, + "client_matchLock":40, + "client_matchChangeSettings":41, + "server_fellowSpectatorJoined":42, + "server_fellowSpectatorLeft":43, + "client_matchStart":44, + "AllPlayersLoaded":45, + "server_matchStart":46, + "client_matchScoreUpdate":47, + "server_matchScoreUpdate":48, + "client_matchComplete":49, + "server_matchTransferHost":50, + "client_matchChangeMods":51, + "client_matchLoadComplete":52, + "server_matchAllPlayersLoaded":53, + "client_matchNoBeatmap":54, + "client_matchNotReady":55, + "client_matchFailed":56, + "server_matchComplete":58, + "client_matchHasBeatmap":59, + "client_matchSkipRequest":60, + "server_matchSkip":61, + "server_unauthorised":62, + "client_channelJoin":63, + "server_channelJoinSuccess":64, + "server_channelInfo":65, + "server_channelKicked":66, + "server_channelAvailableAutojoin":67, + "client_beatmapInfoRequest":68, + "server_beatmapInfoReply":69, + "client_matchTransferHost":70, + "server_supporterGMT":71, + "server_friendsList":72, + "client_friendAdd":73, + "client_friendRemove":74, + "server_protocolVersion":75, + "server_mainMenuIcon":76, + "client_matchChangeTeam":77, + "client_channelPart":78, + "client_receiveUpdates":79, + "server_topBotnet":80, + "server_matchPlayerSkipped":81, + "client_setAwayMessage":82, + "server_userPanel":83, + "IRC_only":84, + "client_userStatsRequest":85, + "server_restart":86, + "client_invite":87, + "server_invite":88, + "server_channelInfoEnd":89, + "client_matchChangePassword":90, + "server_matchChangePassword":91, + "server_silenceEnd":92, + "client_specialMatchInfoRequest":93, + "server_userSilenced":94, + "server_userPresenceSingle":95, + "server_userPresenceBundle":96, + "client_userPresenceRequest":97, + "client_userPresenceRequestAll":98, + "client_userToggleBlockNonFriendPM":99, + "server_userPMBlocked":100, + "server_targetIsSilenced":101, + "server_versionUpdateForced":102, + "server_switchServer":103, + "server_accountRestricted":104, + "server_jumpscare":105, + "client_matchAbort":106, + "server_switchTourneyServer":107, + "client_specialJoinMatchChannel":108, + "client_specialLeaveMatchChannel":109 +} diff --git a/server/serverHandler.js b/server/serverHandler.js new file mode 100644 index 0000000..a3e94c3 --- /dev/null +++ b/server/serverHandler.js @@ -0,0 +1,253 @@ +const osu = require("osu-packet"), + packetIDs = require("./packetIDs.json"), + loginHandler = require("./loginHandler.js"), + parseUserData = require("./util/parseUserData.js"), + userManager = require("./userManager.js"), + User = require("./User.js"), + bakedResponses = require("./bakedResponses.js"), + Streams = require("./Streams.js"); + +global.users = [ + new User(3, "BeanchoBot", "BeanchoBot", new Date().getTime()) +]; + +// Start a loop that gets new data for users from the database for use on the user panel +setInterval(() => { + for (let i = 0; i < global.users.length; i++) { + const User = global.users[i]; + if (User.id == 3) continue; // Ignore the bot + // Bot: :( + + User.getNewUserInformationFromDatabase(); + } +}, 10000); + +// Set the bot's position on the map +global.users[0].location[0] = 50; +global.users[0].location[1] = -32; + +// An array containing all currently active multiplayer matches +global.matches = []; +// An array containing the last 15 messages in chat +// TODO: Bother making this +global.chatHistory = []; +global.StreamsHandler = new Streams(); + +// An array containing all chat channels +// TODO: Send user chat channels and not have osu! crash +global.channels = [ + { channelName:"#osu", channelTopic:"The main channel", channelUserCount: 0 }, + { channelName:"#userlog", channelTopic:"Log about stuff doing go on yes very", channelUserCount: 0 }, + { channelName:"#lobby", channelTopic:"Talk about multiplayer stuff", channelUserCount: 0 }, + { channelName:"#english", channelTopic:"Talk in exclusively English", channelUserCount: 0 }, + { channelName:"#japanese", channelTopic:"Talk in exclusively Japanese", channelUserCount: 0 }, +]; + +// Create a stream for each chat channel +for (let i = 0; i < global.channels.length; i++) { + global.StreamsHandler.addStream(global.channels[i].channelName, false); +} + +// Add a stream for the multiplayer lobby +global.StreamsHandler.addStream("multiplayer_lobby", false); + +// Start stream checking interval +global.StreamsHandler.streamChecker(5000); + +// Include packets +const ChangeAction = require("./Packets/ChangeAction.js"), + SendPublicMessage = require("./Packets/SendPublicMessage.js"), + Logout = require("./Packets/Logout.js"), + Spectator = require("./Spectator.js"), + Multiplayer = require("./Multiplayer.js"), + ChannelPart = require("./Packets/ChannelPart.js"), + UserPresenceBundle = require("./Packets/UserPresenceBundle.js"), + UserPresence = require("./Packets/UserPresence.js"), + UserStatsRequest = require("./Packets/UserStatsRequest.js"); + +module.exports = function(req, res) { + // Get the client's token string and request data + const requestTokenString = req.header("osu-token"), + requestData = req.packet; + + // Server's response & new client token + let responseTokenString = "", + responseData = new Buffer.alloc(0); + + // 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); + global.consoleHelper.printBancho(`New client connection. [User: ${userData.username}]`); + loginHandler(req, res, userData); + } else { + // Client has a token, let's see what they want. + try { + // Get the current user + const userClass = userManager.getUserFromToken(requestTokenString); + + // Make sure the client's token isn't invalid + if (userClass != null) { + // Create a new osu! packet reader + const osuPacketReader = new osu.Client.Reader(requestData); + // Parse current bancho packet + const PacketData = osuPacketReader.Parse(); + // Loop through parsed packet data + for (let i = 0; i < PacketData.length; i++) { + // Get current packet + let CurrentPacket = PacketData[i]; + + // Create a new bancho packet writer per packet + const osuPacketWriter = new osu.Bancho.Writer; + + switch (CurrentPacket.id) { + case packetIDs.client_changeAction: + ChangeAction(userClass, CurrentPacket.data); + break; + + case packetIDs.client_sendPublicMessage: + SendPublicMessage(CurrentPacket, userClass); + break; + + case packetIDs.client_logout: + Logout(userClass); + break; + + case packetIDs.client_requestStatusUpdate: + UserPresenceBundle(userClass); + break; + + case packetIDs.client_pong: + break; + + case packetIDs.client_startSpectating: + Spectator.startSpectatingUser(CurrentPacket.data, userClass); + break; + + case packetIDs.client_spectateFrames: + Spectator.sendSpectatorFrames(userClass, CurrentPacket.data); + break; + + case packetIDs.client_stopSpectating: + Spectator.stopSpectatingUser(userClass); + break; + + case packetIDs.client_joinLobby: + Multiplayer.userEnterLobby(userClass); + break; + + case packetIDs.client_createMatch: + Multiplayer.createMultiplayerMatch(userClass, CurrentPacket.data); + break; + + case packetIDs.client_joinMatch: + Multiplayer.joinMultiplayerMatch(userClass, CurrentPacket.data); + break; + + case packetIDs.client_matchChangeSlot: + Multiplayer.moveToSlot(userClass, CurrentPacket.data); + break; + + case packetIDs.client_matchReady: + Multiplayer.setReadyState(userClass, true); + break; + + case packetIDs.client_matchChangeSettings: + Multiplayer.updateMatch(userClass, CurrentPacket.data); + break; + + case packetIDs.client_matchNotReady: + Multiplayer.setReadyState(userClass, false); + break; + + case packetIDs.client_partMatch: + Multiplayer.leaveMatch(userClass); + break; + + case packetIDs.client_matchLock: // Also handles user kick + Multiplayer.kickPlayer(userClass, CurrentPacket.data); + break; + + case packetIDs.client_matchNoBeatmap: + Multiplayer.missingBeatmap(userClass, true); + break; + + case packetIDs.client_matchHasBeatmap: + Multiplayer.missingBeatmap(userClass, false); + break; + + case packetIDs.client_matchTransferHost: + Multiplayer.transferHost(userClass, CurrentPacket.data); + break; + + case packetIDs.client_matchChangeMods: + Multiplayer.updateMods(userClass, CurrentPacket.data); + break; + + case packetIDs.client_matchStart: + Multiplayer.startMatch(userClass); + break; + + case packetIDs.client_matchLoadComplete: + Multiplayer.setPlayerLoaded(userClass); + break; + + case packetIDs.client_matchComplete: + Multiplayer.onPlayerFinishMatch(userClass); + break; + + case packetIDs.client_matchScoreUpdate: + Multiplayer.updatePlayerScore(userClass, CurrentPacket.data); + break; + + case packetIDs.client_matchFailed: + break; + + case packetIDs.client_channelJoin: + // TODO: Implement user channel joining + // Auto channel joining is already complete + break; + + case packetIDs.client_channelPart: + ChannelPart(userClass, CurrentPacket.data); + break; + + case packetIDs.client_userStatsRequest: + UserStatsRequest(userClass, CurrentPacket.data); + break; + + case packetIDs.client_userPresenceRequest: + UserPresence(userClass, userClass.id); + break; + + default: + // Ignore client_beatmapInfoRequest and client_receiveUpdates + if (CurrentPacket.id == 68 || CurrentPacket.id == 79) break; + // Print out unimplemented packet + console.dir(CurrentPacket); + break; + } + + // Put current user queue into response data + responseData = Buffer.concat([responseData, userClass.queue], responseData.length + userClass.queue.length); + userClass.clearQueue(); + } + } else { + // User's token is invlid, force a reconnect + global.consoleHelper.printBancho(`Forced client re-login (Token is invalid)`); + responseData = bakedResponses("reconnect"); + } + } catch (e) { + console.error(e); + } finally { + // Send the prepared packet to the client + res.writeHead(200, { + "cho-protocol": 19, + "Connection": "keep-alive", + "Keep-Alive": "timeout=5, max=100", + "Content-Type": "text/html; charset=UTF-8" + }); + res.end(responseData); + } + } +}; \ No newline at end of file diff --git a/server/userManager.js b/server/userManager.js new file mode 100644 index 0000000..20e33f0 --- /dev/null +++ b/server/userManager.js @@ -0,0 +1,55 @@ +module.exports = { + getUserFromID:function(id) { + let userToReturn; + for (let i = 0; i < global.users.length; i++) { + if (global.users[i].id == id) { + userToReturn = global.users[i]; + i = global.users.length; + } else { + continue; + } + } + return userToReturn; + }, + + getUserFromUsername:function(username) { + let userToReturn; + for (let i = 0; i < global.users.length; i++) { + if (global.users[i].username == username) { + userToReturn = global.users[i]; + break; + } else { + continue; + } + } + return userToReturn; + }, + + getUserFromToken:function(token) { + let userToReturn; + for (let i = 0; i < global.users.length; i++) { + if (global.users[i].uuid == token) { + userToReturn = global.users[i]; + i = global.users.length; + } else { + continue; + } + } + return userToReturn; + }, + + queueActionForAll:function(action) { + for (let i = 0; i < global.users.length; i++) { + global.users[i].addActionToQueue(action); + } + }, + + queueActionForAllWithoutCallingUser:function(action, token) { + for (let i = 0; i < global.users.length; i++) { + if (global.users[i].uuid == token) { + continue; + } + global.users[i].addActionToQueue(action); + } + } +} \ No newline at end of file diff --git a/server/util/getUserById.js b/server/util/getUserById.js new file mode 100644 index 0000000..0ab43c6 --- /dev/null +++ b/server/util/getUserById.js @@ -0,0 +1,10 @@ +module.exports = function(id) { + let user = null; + for (let i = 0; i < global.users.length; i++) { + if (global.users[i].id == id) { + user = global.users[i]; + break; + } + } + return user; +} \ No newline at end of file diff --git a/server/util/parseUserData.js b/server/util/parseUserData.js new file mode 100644 index 0000000..fe051ed --- /dev/null +++ b/server/util/parseUserData.js @@ -0,0 +1,33 @@ +module.exports = function(packet) { + try { + const p = packet.toString(); // Convert our buffer to a String + const s = p.split('\n'); // Split our Login Data to Username Password Osuversion|blabla|bla + const n = s[2].split('|'); // Split osuversion|blablabla|blablabla to a object. + const username = s[0]; // Username ofc + const password = s[1]; // Password ofc + const osuversion = n[0]; // OsuVersion ofc. + const TimeOffset = Number(n[1]); // Comeon, i dont realy have to tell you what this is. + const clientData = n[3].split(':')[2]; // Some system information. such as MacAdress or DiskID + + // If some data is not set OR is invailed throw errors + if (username == undefined) throw 'UserName'; + if (password == undefined) throw 'password'; + if (osuversion == undefined) throw 'osuversion'; + if (TimeOffset == undefined) throw 'offset'; + if (clientData == undefined) throw 'clientData'; + + // Everything alright? return parsed data. + const obj = { + username: String(username), + password: String(password), + osuversion: String(osuversion), + timeoffset: Number(TimeOffset), + clientdata: String(clientData) + }; + // Here is the return. + return obj; + } catch (ex) { + // Else return undefined, that the login request got broke. + return undefined; + } +} \ No newline at end of file diff --git a/web/serverPage.html b/web/serverPage.html new file mode 100644 index 0000000..45604d6 --- /dev/null +++ b/web/serverPage.html @@ -0,0 +1,18 @@ + + + Binato + + +
+
+  .  o ..
+  o . o o.o                           |isdebug?|
+       ...oo
+          __[]__         Binato
+       __|_o_o_o\__        A custom osu!Bancho
+       \""""""""""/
+        \. ..  . /         Website | Github
+  ^^^^^^^^^^^^^^^^^^^^
+    
+ + \ No newline at end of file