From a6bd08e928211a81b925648228da1e3731023516 Mon Sep 17 00:00:00 2001 From: Nyo Date: Wed, 20 Jul 2016 11:59:53 +0200 Subject: [PATCH] Edited 'last' command to new beatmaps table structure --- constants/exceptions.py | 3 + constants/fokabotCommands.py | 9 +- constants/privileges.py | 1 + events/loginEvent.py | 54 +++++++- handlers/apiVerifiedStatusHandler.py | 46 +++++++ helpers/discordBotHelper.py | 8 ++ helpers/logHelper.py | 32 +++-- helpers/userHelper.py | 176 ++++++++++++++++++++++++++- objects/glob.py | 1 + pep.py | 2 + version | 1 - 11 files changed, 311 insertions(+), 22 deletions(-) create mode 100644 handlers/apiVerifiedStatusHandler.py diff --git a/constants/exceptions.py b/constants/exceptions.py index e353197..9cd1cd1 100644 --- a/constants/exceptions.py +++ b/constants/exceptions.py @@ -80,3 +80,6 @@ class userRestrictedException(Exception): class haxException(Exception): pass + +class forceUpdateException(Exception): + pass diff --git a/constants/fokabotCommands.py b/constants/fokabotCommands.py index 3fb1216..f6addc9 100644 --- a/constants/fokabotCommands.py +++ b/constants/fokabotCommands.py @@ -528,7 +528,7 @@ def tillerinoAcc(fro, chan, message): def tillerinoLast(fro, chan, message): try: data = glob.db.fetch("""SELECT beatmaps.song_name as sn, scores.*, - beatmaps.beatmap_id as bid, beatmaps.difficulty, beatmaps.max_combo as fc + beatmaps.beatmap_id as bid, beatmaps.difficulty_std, beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania, beatmaps.max_combo as fc FROM scores LEFT JOIN beatmaps ON beatmaps.beatmap_md5=scores.beatmap_md5 LEFT JOIN users ON users.id = scores.userid @@ -538,6 +538,7 @@ def tillerinoLast(fro, chan, message): if data == None: return False + diffString = "difficulty_{}".format(gameModes.getGameModeForDB(data["play_mode"])) rank = generalFunctions.getRank(data["play_mode"], data["mods"], data["accuracy"],\ data["300_count"], data["100_count"], data["50_count"], data["misses_count"]) @@ -555,7 +556,7 @@ def tillerinoLast(fro, chan, message): msg += ifFc msg += " | {0:.2f}%, {1}".format(data["accuracy"], rank.upper()) msg += " {{ {0} / {1} / {2} / {3} }}".format(data["300_count"], data["100_count"], data["50_count"], data["misses_count"]) - msg += " | {0:.2f} stars".format(data["difficulty"]) + msg += " | {0:.2f} stars".format(data[diffString]) return msg msg = ifPlayer @@ -566,8 +567,8 @@ def tillerinoLast(fro, chan, message): msg += ifFc msg += " | {0:.2f}pp".format(data["pp"]) - stars = data["difficulty"] - if data["mods"]: + stars = data[diffString] + if data["mods"] and data["play_mode"] == gameModes.std: token = glob.tokens.getTokenFromUsername(fro) if token == None: return False diff --git a/constants/privileges.py b/constants/privileges.py index 165bfc1..13a007b 100644 --- a/constants/privileges.py +++ b/constants/privileges.py @@ -18,3 +18,4 @@ ADMIN_MANAGE_PRIVILEGES = 2 << 15 ADMIN_SEND_ALERTS = 2 << 16 ADMIN_CHAT_MOD = 2 << 17 ADMIN_KICK_USERS = 2 << 18 +USER_PENDING_VERIFICATION = 2 << 19 diff --git a/events/loginEvent.py b/events/loginEvent.py index f66db8e..0f84e1f 100644 --- a/events/loginEvent.py +++ b/events/loginEvent.py @@ -14,6 +14,7 @@ from helpers import requestHelper from helpers import discordBotHelper from helpers import logHelper as log from helpers import chatHelper as chat +from constants import privileges def handle(tornadoRequest): # Data to return @@ -23,6 +24,10 @@ def handle(tornadoRequest): # Get IP from tornado request requestIP = tornadoRequest.getRequestIP() + # Avoid exceptions + clientData = ["unknown", "unknown", "unknown", "unknown", "unknown"] + osuVersion = "unknown" + # Split POST body so we can get username/password/hardware data # 2:-3 thing is because requestData has some escape stuff that we don't need loginData = str(tornadoRequest.request.body)[2:-3].split("\\n") @@ -34,6 +39,19 @@ def handle(tornadoRequest): if len(loginData) < 3: raise exceptions.haxException() + # Get HWID, MAC address and more + # Structure (new line = "|", already split) + # [0] osu! version + # [1] plain mac addressed, separated by "." + # [2] mac addresses hash set + # [3] unique ID + # [4] disk ID + splitData = loginData[2].split("|") + osuVersion = splitData[0] + clientData = splitData[3].split(":")[:5] + if len(clientData) < 4: + raise exceptions.forceUpdateException() + # Try to get the ID from username username = str(loginData[0]) userID = userHelper.getID(username) @@ -46,7 +64,8 @@ def handle(tornadoRequest): raise exceptions.loginFailedException() # Make sure we are not banned - if userHelper.isBanned(userID) == True: + priv = userHelper.getPrivileges(userID) + if userHelper.isBanned(userID) == True and priv & privileges.USER_PENDING_VERIFICATION == 0: raise exceptions.loginBannedException() # 2FA check @@ -55,6 +74,29 @@ def handle(tornadoRequest): raise exceptions.need2FAException() # No login errors! + + # Verify this user (if pending activation) + firstLogin = False + if priv & privileges.USER_PENDING_VERIFICATION > 0 or userHelper.hasVerifiedHardware(userID) == False: + if userHelper.verifyUser(userID, clientData) == True: + # Valid account + log.info("Account {} verified successfully!".format(userID)) + glob.verifiedCache[str(userID)] = 1 + firstLogin = True + else: + # Multiaccount detected + log.info("Account {} NOT verified!".format(userID)) + glob.verifiedCache[str(userID)] = 0 + raise exceptions.loginBannedException() + + # Save HWID in db + hwAllowed = userHelper.logHardware(userID, clientData, firstLogin) + # This is false only if HWID is empty + # if HWID is banned, we get restricted so there's no + # need to deny bancho access + if hwAllowed == False: + raise exceptions.haxException() + # Log user IP userHelper.IPLog(userID, requestIP) @@ -185,6 +227,12 @@ def handle(tornadoRequest): except exceptions.need2FAException: # User tried to log in from unknown IP responseData += serverPackets.needVerification() + except exceptions.haxException: + # Using oldoldold client, we can't check hw. Force update. + # (we don't use enqueue because we don't have a token since login has failed) + err = True + responseData += serverPackets.forceUpdate() + responseData += serverPackets.notification("Hory shitto, your client is TOO old! Nice preistoria! Please turn off the switcher and update it.") except: log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc())) finally: @@ -192,8 +240,8 @@ def handle(tornadoRequest): if len(loginData) < 3: msg = "Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP) else: - msg = "Bancho login request from **{}** for user **{}** ({}) **({})**".format(requestIP, loginData[0], loginData[2], "failed" if err == True else "success") - log.info(msg, True) + msg = "Bancho login request from **{}** for user **{}** _({})_\n_Version: {}\nosu!.exe hash: {}\nMAC: {}\nUID: {}\nHWID: {}_\n".format(requestIP, loginData[0], "failed" if err == True else "success", osuVersion, clientData[0], clientData[2], clientData[3], clientData[4]) + log.info(msg, "bunker") # Return token string and data return (responseTokenString, responseData) diff --git a/handlers/apiVerifiedStatusHandler.py b/handlers/apiVerifiedStatusHandler.py new file mode 100644 index 0000000..bc23526 --- /dev/null +++ b/handlers/apiVerifiedStatusHandler.py @@ -0,0 +1,46 @@ +from helpers import requestHelper +from helpers import logHelper as log +import json +from objects import glob +from constants import exceptions + +class handler(requestHelper.asyncRequestHandler): + def asyncGet(self): + statusCode = 400 + data = {"message": "unknown error"} + try: + # Check arguments + if requestHelper.checkArguments(self.request.arguments, ["u"]) == False: + raise exceptions.invalidArgumentsException() + + # Get userID and its verified cache thing + # -1: Not in cache + # 0: Not verified (multiacc) + # 1: Verified + userID = self.get_argument("u") + data["result"] = -1 if userID not in glob.verifiedCache else glob.verifiedCache[userID] + + # Status code and message + statusCode = 200 + data["message"] = "ok" + except exceptions.invalidArgumentsException: + statusCode = 400 + data["message"] = "missing required arguments" + finally: + # Add status code to data + data["status"] = statusCode + + # Send response + self.add_header("Access-Control-Allow-Origin", "*") + self.add_header("Content-Type", "application/json") + + # jquery meme + output = "" + if "callback" in self.request.arguments: + output += self.get_argument("callback")+"(" + output += json.dumps(data) + if "callback" in self.request.arguments: + output += ")" + + self.write(output) + self.set_status(statusCode) diff --git a/helpers/discordBotHelper.py b/helpers/discordBotHelper.py index 491d143..9a0a961 100644 --- a/helpers/discordBotHelper.py +++ b/helpers/discordBotHelper.py @@ -59,3 +59,11 @@ def sendChatlog(message): message -- message to send """ sendDiscordMessage("chatlog", message, prefix="") + +def sendCM(message): + """ + Send a message to #communitymanagers + + message -- message to send + """ + sendDiscordMessage("cm", message) diff --git a/helpers/logHelper.py b/helpers/logHelper.py index 21ed479..447fc35 100644 --- a/helpers/logHelper.py +++ b/helpers/logHelper.py @@ -1,21 +1,20 @@ from constants import bcolors from helpers import discordBotHelper from helpers import generalFunctions -from helpers.systemHelper import runningUnderUnix from objects import glob from helpers import userHelper import time +import os +ENDL = "\n" if os.name == "posix" else "\r\n" -ENDL = "\n" if runningUnderUnix() else "\r\n" - -def logMessage(message, alertType = "INFO", messageColor = bcolors.ENDC, discord = False, alertDev = False, of = None, stdout = True): +def logMessage(message, alertType = "INFO", messageColor = bcolors.ENDC, discord = None, alertDev = False, of = None, stdout = True): """ Logs a message to stdout/discord/file message -- message to log alertType -- can be any string. Standard types: INFO, WARNING and ERRORS. Defalt: INFO messageColor -- message color (see constants.bcolors). Default = bcolots.ENDC (no color) - discord -- if True, the message will be logged on #bunker channel on discord. Default: False + discord -- discord channel (bunker/cm/staff/general). Optional. Default = None alertDev -- if True, devs will receive an hl on discord. Default: False of -- if not None but a string, log the message to that file (inside .data folder). Eg: "warnings.txt" Default: None (don't log to file) stdout -- if True, print the message to stdout. Default: True @@ -52,8 +51,15 @@ def logMessage(message, alertType = "INFO", messageColor = bcolors.ENDC, discord print(finalMessageConsole) # Log to discord if needed - if discord == True: - discordBotHelper.sendConfidential(message, alertDev) + if discord != None: + if discord == "bunker": + discordBotHelper.sendConfidential(message, alertDev) + elif discord == "cm": + discordBotHelper.sendCM(message) + elif discord == "staff": + discordBotHelper.sendStaff(message) + elif discord == "general": + discordBotHelper.sendGeneral(message) # Log to file if needed if of != None: @@ -64,32 +70,32 @@ def logMessage(message, alertType = "INFO", messageColor = bcolors.ENDC, discord finally: glob.fLocks.unlockFile(of) -def warning(message, discord = False, alertDev = False): +def warning(message, discord = None, alertDev = False): """ Log a warning to stdout, warnings.log (always) and discord (optional) message -- warning message - discord -- if True, send warning to #bunker. Optional. Default = False. + discord -- if not None, send message to that discord channel through schiavo. Optional. Default = None alertDev -- if True, send al hl to devs on discord. Optional. Default = False. """ logMessage(message, "WARNING", bcolors.YELLOW, discord, alertDev, "warnings.txt") -def error(message, discord = False, alertDev = True): +def error(message, discord = None, alertDev = True): """ Log an error to stdout, errors.log (always) and discord (optional) message -- error message - discord -- if True, send error to #bunker. Optional. Default = False. + discord -- if not None, send message to that discord channel through schiavo. Optional. Default = None alertDev -- if True, send al hl to devs on discord. Optional. Default = False. """ logMessage(message, "ERROR", bcolors.RED, discord, alertDev, "errors.txt") -def info(message, discord = False, alertDev = False): +def info(message, discord = None, alertDev = False): """ Log an error to stdout (and info.log) message -- info message - discord -- if True, send error to #bunker. Optional. Default = False. + discord -- if not None, send message to that discord channel through schiavo. Optional. Default = None alertDev -- if True, send al hl to devs on discord. Optional. Default = False. """ logMessage(message, "INFO", bcolors.ENDC, discord, alertDev, "info.txt") diff --git a/helpers/userHelper.py b/helpers/userHelper.py index 62dc64a..f707e72 100644 --- a/helpers/userHelper.py +++ b/helpers/userHelper.py @@ -392,7 +392,7 @@ def ban(userID): userID -- id of user """ banDateTime = int(time.time()) - glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s", [ ~(privileges.USER_NORMAL | privileges.USER_PUBLIC) , banDateTime, userID]) + glob.db.execute("UPDATE users SET privileges = privileges & %s, ban_datetime = %s WHERE id = %s", [ ~(privileges.USER_NORMAL | privileges.USER_PUBLIC | privileges.USER_PENDING_VERIFICATION) , banDateTime, userID]) def unban(userID): """ @@ -433,6 +433,15 @@ def getPrivileges(userID): else: return 0 +def setPrivileges(userID, priv): + """ + Set userID's privileges in db + + userID -- id of user + priv -- privileges number + """ + glob.db.execute("UPDATE users SET privileges = %s WHERE id = %s", [priv, userID]) + def isInPrivilegeGroup(userID, groupName): groupPrivileges = glob.db.fetch("SELECT privileges FROM privileges_groups WHERE name = %s", [groupName]) if groupPrivileges == None: @@ -444,3 +453,168 @@ def isInPrivilegeGroup(userID, groupName): else: userPrivileges = getPrivileges(userID) return (userPrivileges == groupPrivileges) or (userPrivileges == (groupPrivileges | privileges.USER_DONOR)) + + +def appendNotes(userID, notes, addNl = True): + """ + Append "notes" to current userID's "notes for CM" + + userID -- id of user + notes -- text to append + addNl -- if True, prepend \n to notes. Optional. Default: True. + """ + if addNl == True: + notes = "\n"+notes + glob.db.execute("UPDATE users SET notes=CONCAT(COALESCE(notes, ''),%s) WHERE id = %s", [notes, userID]) + + +def logHardware(userID, hashes, activation = False): + """ + Hardware log + + Peppy's botnet structure (new line = "|", already split) + [0] osu! version + [1] plain mac addressed, separated by "." + [2] mac addresses hash set + [3] unique ID + [4] disk ID + + return -- True if hw is not banned, otherwise false + """ + # Make sure the strings are not empty + for i in hashes: + if i == "": + log.warning("Invalid hash set ({}) for user {} in HWID check".format(hashes, userID), "bunk") + return False + + # Run some HWID checks on that user if he is not restricted + if isRestricted(userID) == False: + # Get username + username = getUsername(userID) + + # Get the list of banned or restricted users that have logged in from this or similar HWID hash set + banned = glob.db.fetchAll("""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user + LEFT JOIN users ON users.id = hw_user.userid + WHERE hw_user.userid != %(userid)s + AND (IF(%(mac)s!='b4ec3c4334a0249dae95c284ec5983df', hw_user.mac = %(mac)s, 0) OR hw_user.unique_id = %(uid)s OR hw_user.disk_id = %(diskid)s) + AND (users.privileges & 3 != 3)""", { + "userid": userID, + "mac": hashes[2], + "uid": hashes[3], + "diskid": hashes[4], + }) + + for i in banned: + # Get the total numbers of logins + total = glob.db.fetch("SELECT COUNT(*) AS count FROM hw_user WHERE userid = %s LIMIT 1", [userID]) + # and make sure it is valid + if total == None: + continue + total = total["count"] + + # Calculate 10% of total + perc = (total*10)/100 + + if i["occurencies"] >= perc: + # If the banned user has logged in more than 10% of the times from this user, restrict this user + restrict(userID) + appendNotes(userID, "-- Logged in from HWID ({hwid}) used more than 10% from user {banned} ({bannedUserID}), who is banned/restricted.".format( + hwid=hashes[2:5], + banned=i["username"], + bannedUserID=i["userid"] + )) + log.warning("**{user}** ({userID}) has been restricted because he has logged in from HWID _({hwid})_ used more than 10% from banned/restricted user **{banned}** ({bannedUserID}), **possible multiaccount**.".format( + user=username, + userID=userID, + hwid=hashes[2:5], + banned=i["username"], + bannedUserID=i["userid"] + ), "cm") + + # Update hash set occurencies + glob.db.execute(""" + INSERT INTO hw_user (id, userid, mac, unique_id, disk_id, occurencies) VALUES (NULL, %s, %s, %s, %s, 1) + ON DUPLICATE KEY UPDATE occurencies = occurencies + 1 + """, [userID, hashes[2], hashes[3], hashes[4]]) + + # Optionally, set this hash as 'used for activation' + if activation == True: + glob.db.execute("UPDATE hw_user SET activated = 1 WHERE userid = %s AND mac = %s AND unique_id = %s AND disk_id = %s", [userID, hashes[2], hashes[3], hashes[4]]) + + # Access granted, abbiamo impiegato 3 giorni + # We grant access even in case of login from banned HWID + # because we call restrict() above so there's no need to deny the access. + return True + + +def resetPendingFlag(userID, success=True): + """ + Remove pending flag from an user. + + userID -- ID of the user + success -- if True, set USER_PUBLIC and USER_NORMAL flags too + """ + glob.db.execute("UPDATE users SET privileges = privileges & %s WHERE id = %s LIMIT 1", [~privileges.USER_PENDING_VERIFICATION, userID]) + if success == True: + glob.db.execute("UPDATE users SET privileges = privileges | %s WHERE id = %s LIMIT 1", [(privileges.USER_PUBLIC | privileges.USER_NORMAL), userID]) + +def verifyUser(userID, hashes): + # Check for valid hash set + for i in hashes: + if i == "": + log.warning("Invalid hash set ({}) for user {} while verifying the account".format(str(hashes), userID), "bunk") + return False + + # Get username + username = getUsername(userID) + + # Make sure there are no other accounts activated with this exact mac/unique id/hwid + match = glob.db.fetchAll("SELECT userid FROM hw_user WHERE (IF(%(mac)s != 'b4ec3c4334a0249dae95c284ec5983df', mac = %(mac)s, 0) OR unique_id = %(uid)s OR disk_id = %(diskid)s) AND userid != %(userid)s AND activated = 1 LIMIT 1", { + "mac": hashes[2], + "uid": hashes[3], + "diskid": hashes[4], + "userid": userID + }) + + if match: + # This is a multiaccount, restrict other account and ban this account + + # Get original userID and username (lowest ID) + originalUserID = match[0]["userid"] + originalUsername = getUsername(originalUserID) + + # Ban this user and append notes + ban(userID) # this removes the USER_PENDING_VERIFICATION flag too + appendNotes(userID, "-- {}'s multiaccount ({}), found HWID match while verifying account ({})".format(originalUsername, originalUserID, hashes[2:5])) + appendNotes(originalUserID, "-- Has created multiaccount {} ({})".format(username, userID)) + + # Restrict the original + restrict(originalUserID) + + # Discord message + log.warning("User **{originalUsername}** ({originalUserID}) has been restricted because he has created multiaccount **{username}** ({userID}). The multiaccount has been banned.".format( + originalUsername=originalUsername, + originalUserID=originalUserID, + username=username, + userID=userID + ), "cm") + + # Disallow login + return False + else: + # No matches found, set USER_PUBLIC and USER_NORMAL flags and reset USER_PENDING_VERIFICATION flag + resetPendingFlag(userID) + log.info("User **{}** ({}) has verified his account with hash set _{}_".format(username, userID, hashes[2:5]), "cm") + + # Allow login + return True + +def hasVerifiedHardware(userID): + """ + userID -- id of the user + return -- True if hwid activation data is in db, otherwise false + """ + data = glob.db.fetch("SELECT id FROM hw_user WHERE userid = %s AND activated = 1 LIMIT 1", [userID]) + if data != None: + return True + return False diff --git a/objects/glob.py b/objects/glob.py index 404ad88..93836d7 100644 --- a/objects/glob.py +++ b/objects/glob.py @@ -22,6 +22,7 @@ matches = matchList.matchList() restarting = False pool = None fLocks = fileLocks.fileLocks() +verifiedCache = {} cloudflare = False debug = False diff --git a/pep.py b/pep.py index 0966827..6ca541c 100644 --- a/pep.py +++ b/pep.py @@ -30,6 +30,7 @@ from handlers import apiIsOnlineHandler from handlers import apiOnlineUsersHandler from handlers import apiServerStatusHandler from handlers import ciTriggerHandler +from handlers import apiVerifiedStatusHandler from irc import ircserver @@ -40,6 +41,7 @@ def make_app(): (r"/api/v1/onlineUsers", apiOnlineUsersHandler.handler), (r"/api/v1/serverStatus", apiServerStatusHandler.handler), (r"/api/v1/ciTrigger", ciTriggerHandler.handler), + (r"/api/v1/verifiedStatus", apiVerifiedStatusHandler.handler), ]) if __name__ == "__main__": diff --git a/version b/version index 400084b..e69de29 100644 --- a/version +++ b/version @@ -1 +0,0 @@ -1.6.7