Edited 'last' command to new beatmaps table structure

This commit is contained in:
Nyo 2016-07-20 11:59:53 +02:00
parent 94d46cf45f
commit a6bd08e928
11 changed files with 311 additions and 22 deletions

View File

@ -80,3 +80,6 @@ class userRestrictedException(Exception):
class haxException(Exception): class haxException(Exception):
pass pass
class forceUpdateException(Exception):
pass

View File

@ -528,7 +528,7 @@ def tillerinoAcc(fro, chan, message):
def tillerinoLast(fro, chan, message): def tillerinoLast(fro, chan, message):
try: try:
data = glob.db.fetch("""SELECT beatmaps.song_name as sn, scores.*, 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 FROM scores
LEFT JOIN beatmaps ON beatmaps.beatmap_md5=scores.beatmap_md5 LEFT JOIN beatmaps ON beatmaps.beatmap_md5=scores.beatmap_md5
LEFT JOIN users ON users.id = scores.userid LEFT JOIN users ON users.id = scores.userid
@ -538,6 +538,7 @@ def tillerinoLast(fro, chan, message):
if data == None: if data == None:
return False return False
diffString = "difficulty_{}".format(gameModes.getGameModeForDB(data["play_mode"]))
rank = generalFunctions.getRank(data["play_mode"], data["mods"], data["accuracy"],\ rank = generalFunctions.getRank(data["play_mode"], data["mods"], data["accuracy"],\
data["300_count"], data["100_count"], data["50_count"], data["misses_count"]) data["300_count"], data["100_count"], data["50_count"], data["misses_count"])
@ -555,7 +556,7 @@ def tillerinoLast(fro, chan, message):
msg += ifFc msg += ifFc
msg += " | {0:.2f}%, {1}".format(data["accuracy"], rank.upper()) 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} / {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 return msg
msg = ifPlayer msg = ifPlayer
@ -566,8 +567,8 @@ def tillerinoLast(fro, chan, message):
msg += ifFc msg += ifFc
msg += " | {0:.2f}pp".format(data["pp"]) msg += " | {0:.2f}pp".format(data["pp"])
stars = data["difficulty"] stars = data[diffString]
if data["mods"]: if data["mods"] and data["play_mode"] == gameModes.std:
token = glob.tokens.getTokenFromUsername(fro) token = glob.tokens.getTokenFromUsername(fro)
if token == None: if token == None:
return False return False

View File

@ -18,3 +18,4 @@ ADMIN_MANAGE_PRIVILEGES = 2 << 15
ADMIN_SEND_ALERTS = 2 << 16 ADMIN_SEND_ALERTS = 2 << 16
ADMIN_CHAT_MOD = 2 << 17 ADMIN_CHAT_MOD = 2 << 17
ADMIN_KICK_USERS = 2 << 18 ADMIN_KICK_USERS = 2 << 18
USER_PENDING_VERIFICATION = 2 << 19

View File

@ -14,6 +14,7 @@ from helpers import requestHelper
from helpers import discordBotHelper from helpers import discordBotHelper
from helpers import logHelper as log from helpers import logHelper as log
from helpers import chatHelper as chat from helpers import chatHelper as chat
from constants import privileges
def handle(tornadoRequest): def handle(tornadoRequest):
# Data to return # Data to return
@ -23,6 +24,10 @@ def handle(tornadoRequest):
# Get IP from tornado request # Get IP from tornado request
requestIP = tornadoRequest.getRequestIP() requestIP = tornadoRequest.getRequestIP()
# Avoid exceptions
clientData = ["unknown", "unknown", "unknown", "unknown", "unknown"]
osuVersion = "unknown"
# Split POST body so we can get username/password/hardware data # 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 # 2:-3 thing is because requestData has some escape stuff that we don't need
loginData = str(tornadoRequest.request.body)[2:-3].split("\\n") loginData = str(tornadoRequest.request.body)[2:-3].split("\\n")
@ -34,6 +39,19 @@ def handle(tornadoRequest):
if len(loginData) < 3: if len(loginData) < 3:
raise exceptions.haxException() 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 # Try to get the ID from username
username = str(loginData[0]) username = str(loginData[0])
userID = userHelper.getID(username) userID = userHelper.getID(username)
@ -46,7 +64,8 @@ def handle(tornadoRequest):
raise exceptions.loginFailedException() raise exceptions.loginFailedException()
# Make sure we are not banned # 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() raise exceptions.loginBannedException()
# 2FA check # 2FA check
@ -55,6 +74,29 @@ def handle(tornadoRequest):
raise exceptions.need2FAException() raise exceptions.need2FAException()
# No login errors! # 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 # Log user IP
userHelper.IPLog(userID, requestIP) userHelper.IPLog(userID, requestIP)
@ -185,6 +227,12 @@ def handle(tornadoRequest):
except exceptions.need2FAException: except exceptions.need2FAException:
# User tried to log in from unknown IP # User tried to log in from unknown IP
responseData += serverPackets.needVerification() 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: except:
log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc())) log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
finally: finally:
@ -192,8 +240,8 @@ def handle(tornadoRequest):
if len(loginData) < 3: if len(loginData) < 3:
msg = "Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP) msg = "Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP)
else: else:
msg = "Bancho login request from **{}** for user **{}** ({}) **({})**".format(requestIP, loginData[0], loginData[2], "failed" if err == True else "success") 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, True) log.info(msg, "bunker")
# Return token string and data # Return token string and data
return (responseTokenString, responseData) return (responseTokenString, responseData)

View File

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

View File

@ -59,3 +59,11 @@ def sendChatlog(message):
message -- message to send message -- message to send
""" """
sendDiscordMessage("chatlog", message, prefix="") sendDiscordMessage("chatlog", message, prefix="")
def sendCM(message):
"""
Send a message to #communitymanagers
message -- message to send
"""
sendDiscordMessage("cm", message)

View File

@ -1,21 +1,20 @@
from constants import bcolors from constants import bcolors
from helpers import discordBotHelper from helpers import discordBotHelper
from helpers import generalFunctions from helpers import generalFunctions
from helpers.systemHelper import runningUnderUnix
from objects import glob from objects import glob
from helpers import userHelper from helpers import userHelper
import time 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 = None, alertDev = False, of = None, stdout = True):
def logMessage(message, alertType = "INFO", messageColor = bcolors.ENDC, discord = False, alertDev = False, of = None, stdout = True):
""" """
Logs a message to stdout/discord/file Logs a message to stdout/discord/file
message -- message to log message -- message to log
alertType -- can be any string. Standard types: INFO, WARNING and ERRORS. Defalt: INFO alertType -- can be any string. Standard types: INFO, WARNING and ERRORS. Defalt: INFO
messageColor -- message color (see constants.bcolors). Default = bcolots.ENDC (no color) 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 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) 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 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) print(finalMessageConsole)
# Log to discord if needed # Log to discord if needed
if discord == True: if discord != None:
if discord == "bunker":
discordBotHelper.sendConfidential(message, alertDev) 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 # Log to file if needed
if of != None: if of != None:
@ -64,32 +70,32 @@ def logMessage(message, alertType = "INFO", messageColor = bcolors.ENDC, discord
finally: finally:
glob.fLocks.unlockFile(of) 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) Log a warning to stdout, warnings.log (always) and discord (optional)
message -- warning message 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. alertDev -- if True, send al hl to devs on discord. Optional. Default = False.
""" """
logMessage(message, "WARNING", bcolors.YELLOW, discord, alertDev, "warnings.txt") 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) Log an error to stdout, errors.log (always) and discord (optional)
message -- error message 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. alertDev -- if True, send al hl to devs on discord. Optional. Default = False.
""" """
logMessage(message, "ERROR", bcolors.RED, discord, alertDev, "errors.txt") 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) Log an error to stdout (and info.log)
message -- info message 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. alertDev -- if True, send al hl to devs on discord. Optional. Default = False.
""" """
logMessage(message, "INFO", bcolors.ENDC, discord, alertDev, "info.txt") logMessage(message, "INFO", bcolors.ENDC, discord, alertDev, "info.txt")

View File

@ -392,7 +392,7 @@ def ban(userID):
userID -- id of user userID -- id of user
""" """
banDateTime = int(time.time()) 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): def unban(userID):
""" """
@ -433,6 +433,15 @@ def getPrivileges(userID):
else: else:
return 0 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): def isInPrivilegeGroup(userID, groupName):
groupPrivileges = glob.db.fetch("SELECT privileges FROM privileges_groups WHERE name = %s", [groupName]) groupPrivileges = glob.db.fetch("SELECT privileges FROM privileges_groups WHERE name = %s", [groupName])
if groupPrivileges == None: if groupPrivileges == None:
@ -444,3 +453,168 @@ def isInPrivilegeGroup(userID, groupName):
else: else:
userPrivileges = getPrivileges(userID) userPrivileges = getPrivileges(userID)
return (userPrivileges == groupPrivileges) or (userPrivileges == (groupPrivileges | privileges.USER_DONOR)) 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

View File

@ -22,6 +22,7 @@ matches = matchList.matchList()
restarting = False restarting = False
pool = None pool = None
fLocks = fileLocks.fileLocks() fLocks = fileLocks.fileLocks()
verifiedCache = {}
cloudflare = False cloudflare = False
debug = False debug = False

2
pep.py
View File

@ -30,6 +30,7 @@ from handlers import apiIsOnlineHandler
from handlers import apiOnlineUsersHandler from handlers import apiOnlineUsersHandler
from handlers import apiServerStatusHandler from handlers import apiServerStatusHandler
from handlers import ciTriggerHandler from handlers import ciTriggerHandler
from handlers import apiVerifiedStatusHandler
from irc import ircserver from irc import ircserver
@ -40,6 +41,7 @@ def make_app():
(r"/api/v1/onlineUsers", apiOnlineUsersHandler.handler), (r"/api/v1/onlineUsers", apiOnlineUsersHandler.handler),
(r"/api/v1/serverStatus", apiServerStatusHandler.handler), (r"/api/v1/serverStatus", apiServerStatusHandler.handler),
(r"/api/v1/ciTrigger", ciTriggerHandler.handler), (r"/api/v1/ciTrigger", ciTriggerHandler.handler),
(r"/api/v1/verifiedStatus", apiVerifiedStatusHandler.handler),
]) ])
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1 +0,0 @@
1.6.7