diff --git a/README.md b/README.md index ef0c6d6..a9f04a8 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ This is Ripple's bancho server. It handles: ## Requirements - Python 3.5 - MySQLdb (`mysqlclient` or `mysql-python`) -- Bottle +- Tornado - Gevent - Bcrypt ## How to set up pep.py First of all, install all the dependencies ``` -$ pip install mysqlclient bottle gevent bcrypt +$ pip install mysqlclient tornado gevent bcrypt ``` then, run pep.py once to create the default config file and edit it ``` diff --git a/constants/fokabotCommands.py b/constants/fokabotCommands.py index 3d14cb0..1390c37 100644 --- a/constants/fokabotCommands.py +++ b/constants/fokabotCommands.py @@ -586,7 +586,6 @@ def tillerinoLast(fro, chan, message): stars = oppaiData["stars"] msg += " | {0:.2f} stars".format(stars) - return msg except Exception as a: log.error(a) diff --git a/events/loginEvent.py b/events/loginEvent.py index 2a3a5a4..2373d24 100644 --- a/events/loginEvent.py +++ b/events/loginEvent.py @@ -2,22 +2,27 @@ from helpers import userHelper from constants import serverPackets from constants import exceptions from objects import glob +from helpers import consoleHelper +from constants import bcolors from helpers import locationHelper from helpers import countryHelper +import time +from helpers import generalFunctions import sys import traceback +from helpers import requestHelper +from helpers import discordBotHelper from helpers import logHelper as log from helpers import chatHelper as chat from constants import privileges -from helpers import requestHelper -def handle(bottleRequest): +def handle(tornadoRequest): # Data to return responseTokenString = "ayy" responseData = bytes() # Get IP from tornado request - requestIP = requestHelper.getRequestIP(bottleRequest) + requestIP = tornadoRequest.getRequestIP() # Avoid exceptions clientData = ["unknown", "unknown", "unknown", "unknown", "unknown"] @@ -25,8 +30,7 @@ def handle(bottleRequest): # 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 - postBody = bottleRequest.body.read() - loginData = str(postBody)[2:-3].split("\\n") + loginData = str(tornadoRequest.request.body)[2:-3].split("\\n") try: # If true, print error to console err = False @@ -244,4 +248,4 @@ def handle(bottleRequest): log.info(msg, "bunker") # Return token string and data - return (responseTokenString, responseData) + return (responseTokenString, responseData) \ No newline at end of file diff --git a/handlers/apiFokabotMessageHandler.py b/handlers/apiFokabotMessageHandler.py index fc5eee0..7024fb6 100644 --- a/handlers/apiFokabotMessageHandler.py +++ b/handlers/apiFokabotMessageHandler.py @@ -1,37 +1,39 @@ +from helpers import requestHelper from constants import exceptions import json from objects import glob from helpers import chatHelper -import bottle +from helpers import logHelper as log -@bottle.route("/api/v1/fokabotMessage") -def GETApiFokabotMessage(): - statusCode = 400 - data = {"message": "unknown error"} - try: - # Check arguments - if "k" not in bottle.request.query or "to" not in bottle.request.query or "msg" not in bottle.request.query: - raise exceptions.invalidArgumentsException() - - # Check ci key - key = bottle.request.query["k"] - if key is None or key != glob.conf.config["server"]["cikey"]: - raise exceptions.invalidArgumentsException() - - # Send chat message - chatHelper.sendMessage("FokaBot", bottle.request.query["to"], bottle.request.query["msg"]) - - # Status code and message - statusCode = 200 - data["message"] = "ok" - except exceptions.invalidArgumentsException: +class handler(requestHelper.asyncRequestHandler): + def asyncGet(self): statusCode = 400 - data["message"] = "invalid parameters" - finally: - # Add status code to data - data["status"] = statusCode + data = {"message": "unknown error"} + try: + # Check arguments + if requestHelper.checkArguments(self.request.arguments, ["k", "to", "msg"]) == False: + raise exceptions.invalidArgumentsException() - # Send response - bottle.response.status = statusCode - bottle.response.add_header("Content-Type", "application/json") - yield json.dumps(data) + # Check ci key + key = self.get_argument("k") + if key is None or key != glob.conf.config["server"]["cikey"]: + raise exceptions.invalidArgumentsException() + + log.info("API REQUEST FOR FOKABOT MESSAGE AAAAAAA") + chatHelper.sendMessage("FokaBot", self.get_argument("to"), self.get_argument("msg")) + + # Status code and message + statusCode = 200 + data["message"] = "ok" + except exceptions.invalidArgumentsException: + statusCode = 400 + data["message"] = "invalid parameters" + finally: + # Add status code to data + data["status"] = statusCode + + # Send response + #self.clear() + self.write(json.dumps(data)) + self.set_status(statusCode) + #self.finish(json.dumps(data)) diff --git a/handlers/apiIsOnlineHandler.py b/handlers/apiIsOnlineHandler.py index c630660..b443821 100644 --- a/handlers/apiIsOnlineHandler.py +++ b/handlers/apiIsOnlineHandler.py @@ -1,39 +1,48 @@ +from helpers import requestHelper from constants import exceptions import json from objects import glob -import bottle -@bottle.route("/api/v1/isOnline") -def GETApiIsOnline(): - statusCode = 400 - data = {"message": "unknown error"} - try: - # Check arguments - if "u" not in bottle.request.query and "id" not in bottle.request.query: - raise exceptions.invalidArgumentsException() - - # Get online staus - if "u" in bottle.request.query: - username = bottle.request.query["u"] - data["result"] = True if glob.tokens.getTokenFromUsername(username) != None else False - else: - try: - userID = int(bottle.request.query["id"]) - data["result"] = True if glob.tokens.getTokenFromUserID(userID) != None else False - except: +class handler(requestHelper.asyncRequestHandler): + def asyncGet(self): + statusCode = 400 + data = {"message": "unknown error"} + try: + # Check arguments + if "u" not in self.request.arguments and "id" not in self.request.arguments: raise exceptions.invalidArgumentsException() - # 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 + # Get online staus + username = None + userID = None + if "u" in self.request.arguments: + username = self.get_argument("u") + else: + try: + userID = int(self.get_argument("id")) + except: + raise exceptions.invalidArgumentsException() - # Send response - bottle.response.status = statusCode - bottle.response.add_header("Content-Type", "application/json") - yield json.dumps(data) + if username == None and userID == None: + data["result"] = False + else: + if username != None: + data["result"] = True if glob.tokens.getTokenFromUsername(username) != None else False + else: + data["result"] = True if glob.tokens.getTokenFromUserID(userID) != None else False + + # 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.clear() + self.write(json.dumps(data)) + self.set_status(statusCode) + #self.finish(json.dumps(data)) diff --git a/handlers/apiOnlineUsersHandler.py b/handlers/apiOnlineUsersHandler.py index 6f40aed..d40f17b 100644 --- a/handlers/apiOnlineUsersHandler.py +++ b/handlers/apiOnlineUsersHandler.py @@ -1,23 +1,24 @@ +from helpers import requestHelper import json from objects import glob -import bottle -@bottle.route("/api/v1/onlineUsers") -def GETApiOnlineUsers(): - statusCode = 400 - data = {"message": "unknown error"} - try: - # Get online users count - data["result"] = len(glob.tokens.tokens) +class handler(requestHelper.asyncRequestHandler): + def asyncGet(self): + statusCode = 400 + data = {"message": "unknown error"} + try: + # Get online users count + data["result"] = len(glob.tokens.tokens) - # Status code and message - statusCode = 200 - data["message"] = "ok" - finally: - # Add status code to data - data["status"] = statusCode + # Status code and message + statusCode = 200 + data["message"] = "ok" + finally: + # Add status code to data + data["status"] = statusCode - # Send response - bottle.response.status = statusCode - bottle.response.add_header("Content-Type", "application/json") - yield json.dumps(data) + # Send response + #self.clear() + self.write(json.dumps(data)) + self.set_status(statusCode) + #self.finish(json.dumps(data)) diff --git a/handlers/apiServerStatusHandler.py b/handlers/apiServerStatusHandler.py index 7158f75..38471bc 100644 --- a/handlers/apiServerStatusHandler.py +++ b/handlers/apiServerStatusHandler.py @@ -1,23 +1,24 @@ +from helpers import requestHelper import json from objects import glob -import bottle -@bottle.route("/api/v1/serverStatus") -def GETApiServerStatus(): - statusCode = 400 - data = {"message": "unknown error"} - try: - # Get online users count - data["result"] = -1 if glob.restarting == True else 1 +class handler(requestHelper.asyncRequestHandler): + def asyncGet(self): + statusCode = 400 + data = {"message": "unknown error"} + try: + # Get online users count + data["result"] = -1 if glob.restarting == True else 1 - # Status code and message - statusCode = 200 - data["message"] = "ok" - finally: - # Add status code to data - data["status"] = statusCode + # Status code and message + statusCode = 200 + data["message"] = "ok" + finally: + # Add status code to data + data["status"] = statusCode - # Send response - bottle.response.status = statusCode - bottle.response.add_header("Content-Type", "application/json") - yield json.dumps(data) + # Send response + #self.clear() + self.write(json.dumps(data)) + self.set_status(statusCode) + #self.finish(json.dumps(data)) diff --git a/handlers/apiVerifiedStatusHandler.py b/handlers/apiVerifiedStatusHandler.py index 4aa2f9e..bc23526 100644 --- a/handlers/apiVerifiedStatusHandler.py +++ b/handlers/apiVerifiedStatusHandler.py @@ -1,48 +1,46 @@ +from helpers import requestHelper +from helpers import logHelper as log import json from objects import glob from constants import exceptions -import bottle -@bottle.route("/api/v1/verifiedStatus") -def GETApiVerifiedStatus(): - statusCode = 400 - data = {"message": "unknown error"} - try: - # Check arguments - if "u" not in bottle.request.query: - raise exceptions.invalidArgumentsException() - - # Get userID and its verified cache thing - # -1: Not in cache - # 0: Not verified (multiacc) - # 1: Verified - userID = bottle.request.query["u"] - callback = None - if "callback" in bottle.request.query: - callback = bottle.request.query["callback"] - 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: +class handler(requestHelper.asyncRequestHandler): + def asyncGet(self): statusCode = 400 - data["message"] = "missing required arguments" - finally: - # Add status code to data - data["status"] = statusCode + data = {"message": "unknown error"} + try: + # Check arguments + if requestHelper.checkArguments(self.request.arguments, ["u"]) == False: + raise exceptions.invalidArgumentsException() - # Send response - bottle.response.add_header("Access-Control-Allow-Origin", "*") - bottle.response.add_header("Content-Type", "application/json") + # 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] - # jquery meme - output = "" - if callback != None: - output += callback+"(" - output += json.dumps(data) - if callback != None: - output += ")" + # 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 - bottle.response.status = statusCode - yield output + # 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/handlers/ciTriggerHandler.py b/handlers/ciTriggerHandler.py index cd44b5d..5ffe294 100644 --- a/handlers/ciTriggerHandler.py +++ b/handlers/ciTriggerHandler.py @@ -1,38 +1,39 @@ +from helpers import requestHelper from constants import exceptions import json from objects import glob from helpers import systemHelper from helpers import logHelper as log -import bottle -@bottle.route("/api/v1/ciTrigger") -def GETCiTrigger(): - statusCode = 400 - data = {"message": "unknown error"} - try: - # Check arguments - if "k" not in bottle.request.query: - raise exceptions.invalidArgumentsException() +class handler(requestHelper.asyncRequestHandler): + def asyncGet(self): + statusCode = 400 + data = {"message": "unknown error"} + try: + # Check arguments + if requestHelper.checkArguments(self.request.arguments, ["k"]) == False: + raise exceptions.invalidArgumentsException() - # Check ci key - key = bottle.request.query["k"] - if key != glob.conf.config["server"]["cikey"]: - raise exceptions.invalidArgumentsException() + # Check ci key + key = self.get_argument("k") + if key is None or key != glob.conf.config["server"]["cikey"]: + raise exceptions.invalidArgumentsException() - log.info("Ci event triggered!!") - systemHelper.scheduleShutdown(5, False, "A new Bancho update is available and the server will be restarted in 5 seconds. Thank you for your patience.") + log.info("Ci event triggered!!") + systemHelper.scheduleShutdown(5, False, "A new Bancho update is available and the server will be restarted in 5 seconds. Thank you for your patience.") - # Status code and message - statusCode = 200 - data["message"] = "ok" - except exceptions.invalidArgumentsException: - statusCode = 403 - data["message"] = "invalid ci key" - finally: - # Add status code to data - data["status"] = statusCode + # Status code and message + statusCode = 200 + data["message"] = "ok" + except exceptions.invalidArgumentsException: + statusCode = 400 + data["message"] = "invalid ci key" + finally: + # Add status code to data + data["status"] = statusCode - # Send response - bottle.response.status = statusCode - bottle.response.add_header("Content-Type", "application/json") - yield json.dumps(data) + # Send response + #self.clear() + self.write(json.dumps(data)) + self.set_status(statusCode) + #self.finish(json.dumps(data)) diff --git a/handlers/mainHandler.py b/handlers/mainHandler.py index 3723242..27bfedc 100644 --- a/handlers/mainHandler.py +++ b/handlers/mainHandler.py @@ -1,11 +1,10 @@ -import bottle import datetime import gzip +from helpers import requestHelper from objects import glob -from helpers import packetHelper -from helpers import logHelper as log from constants import exceptions from constants import packetIDs +from helpers import packetHelper from constants import serverPackets from events import sendPublicMessageEvent from events import sendPrivateMessageEvent @@ -47,199 +46,219 @@ from events import userStatsRequestEvent from events import requestStatusUpdateEvent from events import userPanelRequestEvent -@bottle.route("/", method="POST") -def POSTMain(): - # Track time if needed - if glob.outputRequestTime == True: - # Start time - st = datetime.datetime.now() +# Exception tracking +import tornado.web +import tornado.gen +import sys +import traceback +from raven.contrib.tornado import SentryMixin +from helpers import logHelper as log - # Client's token string and request data - #requestTokenString = bottle.request.headers.get("osu-token") - requestTokenString = bottle.request.get_header("osu-token") - requestData = bottle.request.body.read() - - # Server's token string and request data - responseTokenString = "ayy" - responseData = bytes() - - if requestTokenString == None: - # No token, first request. Handle login. - responseTokenString, responseData = loginEvent.handle(bottle.request) - else: - userToken = None # default value +class handler(SentryMixin, requestHelper.asyncRequestHandler): + @tornado.web.asynchronous + @tornado.gen.engine + def asyncPost(self): try: - # This is not the first packet, send response based on client's request - # Packet start position, used to read stacked packets - pos = 0 + # Track time if needed + if glob.outputRequestTime == True: + # Start time + st = datetime.datetime.now() - # Make sure the token exists - if requestTokenString not in glob.tokens.tokens: - raise exceptions.tokenNotFoundException() + # Client's token string and request data + requestTokenString = self.request.headers.get("osu-token") + requestData = self.request.body - # Token exists, get its object and lock it - userToken = glob.tokens.tokens[requestTokenString] - userToken.lock.acquire() + # Server's token string and request data + responseTokenString = "ayy" + responseData = bytes() - # Keep reading packets until everything has been read - while pos < len(requestData): - # Get packet from stack starting from new packet - leftData = requestData[pos:] + if requestTokenString == None: + # No token, first request. Handle login. + responseTokenString, responseData = loginEvent.handle(self) + else: + userToken = None # default value + try: + # This is not the first packet, send response based on client's request + # Packet start position, used to read stacked packets + pos = 0 - # Get packet ID, data length and data - packetID = packetHelper.readPacketID(leftData) - dataLength = packetHelper.readPacketLength(leftData) - packetData = requestData[pos:(pos+dataLength+7)] + # Make sure the token exists + if requestTokenString not in glob.tokens.tokens: + raise exceptions.tokenNotFoundException() - # Console output if needed - if glob.outputPackets == True and packetID != 4: - log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData))) + # Token exists, get its object and lock it + userToken = glob.tokens.tokens[requestTokenString] + userToken.lock.acquire() - # Event handler - def handleEvent(ev): - def wrapper(): - ev.handle(userToken, packetData) - return wrapper + # Keep reading packets until everything has been read + while pos < len(requestData): + # Get packet from stack starting from new packet + leftData = requestData[pos:] - eventHandler = { - # TODO: Rename packets and events - # TODO: Host check for multi - packetIDs.client_changeAction: handleEvent(changeActionEvent), - packetIDs.client_logout: handleEvent(logoutEvent), - packetIDs.client_friendAdd: handleEvent(friendAddEvent), - packetIDs.client_friendRemove: handleEvent(friendRemoveEvent), - packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent), - packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent), - packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent), - - packetIDs.client_channelJoin: handleEvent(channelJoinEvent), - packetIDs.client_channelPart: handleEvent(channelPartEvent), - packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent), - packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent), - packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent), + # Get packet ID, data length and data + packetID = packetHelper.readPacketID(leftData) + dataLength = packetHelper.readPacketLength(leftData) + packetData = requestData[pos:(pos+dataLength+7)] - packetIDs.client_startSpectating: handleEvent(startSpectatingEvent), - packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent), - packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent), - packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent), + # Console output if needed + if glob.outputPackets == True and packetID != 4: + log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData))) - packetIDs.client_joinLobby: handleEvent(joinLobbyEvent), - packetIDs.client_partLobby: handleEvent(partLobbyEvent), - packetIDs.client_createMatch: handleEvent(createMatchEvent), - packetIDs.client_joinMatch: handleEvent(joinMatchEvent), - packetIDs.client_partMatch: handleEvent(partMatchEvent), - packetIDs.client_matchChangeSlot: handleEvent(changeSlotEvent), - packetIDs.client_matchChangeSettings: handleEvent(changeMatchSettingsEvent), - packetIDs.client_matchChangePassword: handleEvent(changeMatchPasswordEvent), - packetIDs.client_matchChangeMods: handleEvent(changeMatchModsEvent), - packetIDs.client_matchReady: handleEvent(matchReadyEvent), - packetIDs.client_matchNotReady: handleEvent(matchReadyEvent), - packetIDs.client_matchLock: handleEvent(matchLockEvent), - packetIDs.client_matchStart: handleEvent(matchStartEvent), - packetIDs.client_matchLoadComplete: handleEvent(matchPlayerLoadEvent), - packetIDs.client_matchSkipRequest: handleEvent(matchSkipEvent), - packetIDs.client_matchScoreUpdate: handleEvent(matchFramesEvent), - packetIDs.client_matchComplete: handleEvent(matchCompleteEvent), - packetIDs.client_matchNoBeatmap: handleEvent(matchNoBeatmapEvent), - packetIDs.client_matchHasBeatmap: handleEvent(matchHasBeatmapEvent), - packetIDs.client_matchTransferHost: handleEvent(matchTransferHostEvent), - packetIDs.client_matchFailed: handleEvent(matchFailedEvent), - packetIDs.client_matchChangeTeam: handleEvent(matchChangeTeamEvent), - packetIDs.client_invite: handleEvent(matchInviteEvent), - } + # Event handler + def handleEvent(ev): + def wrapper(): + ev.handle(userToken, packetData) + return wrapper - # Packets processed if in restricted mode. - # All other packets will be ignored if the user is in restricted mode - packetsRestricted = [ - packetIDs.client_logout, - packetIDs.client_userStatsRequest, - packetIDs.client_requestStatusUpdate, - packetIDs.client_userPanelRequest, - packetIDs.client_changeAction, - packetIDs.client_channelJoin, - packetIDs.client_channelPart, - ] + eventHandler = { + # TODO: Rename packets and events + # TODO: Host check for multi + packetIDs.client_changeAction: handleEvent(changeActionEvent), + packetIDs.client_logout: handleEvent(logoutEvent), + packetIDs.client_friendAdd: handleEvent(friendAddEvent), + packetIDs.client_friendRemove: handleEvent(friendRemoveEvent), + packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent), + packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent), + packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent), + + packetIDs.client_channelJoin: handleEvent(channelJoinEvent), + packetIDs.client_channelPart: handleEvent(channelPartEvent), + packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent), + packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent), + packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent), - # Process/ignore packet - if packetID != 4: - if packetID in eventHandler: - if userToken.restricted == False or (userToken.restricted == True and packetID in packetsRestricted): - eventHandler[packetID]() - else: - log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID)) - else: - log.warning("Unknown packet id from {} ({})".format(requestTokenString, packetID)) + packetIDs.client_startSpectating: handleEvent(startSpectatingEvent), + packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent), + packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent), + packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent), - # Update pos so we can read the next stacked packet - # +7 because we add packet ID bytes, unused byte and data length bytes - pos += dataLength+7 + packetIDs.client_joinLobby: handleEvent(joinLobbyEvent), + packetIDs.client_partLobby: handleEvent(partLobbyEvent), + packetIDs.client_createMatch: handleEvent(createMatchEvent), + packetIDs.client_joinMatch: handleEvent(joinMatchEvent), + packetIDs.client_partMatch: handleEvent(partMatchEvent), + packetIDs.client_matchChangeSlot: handleEvent(changeSlotEvent), + packetIDs.client_matchChangeSettings: handleEvent(changeMatchSettingsEvent), + packetIDs.client_matchChangePassword: handleEvent(changeMatchPasswordEvent), + packetIDs.client_matchChangeMods: handleEvent(changeMatchModsEvent), + packetIDs.client_matchReady: handleEvent(matchReadyEvent), + packetIDs.client_matchNotReady: handleEvent(matchReadyEvent), + packetIDs.client_matchLock: handleEvent(matchLockEvent), + packetIDs.client_matchStart: handleEvent(matchStartEvent), + packetIDs.client_matchLoadComplete: handleEvent(matchPlayerLoadEvent), + packetIDs.client_matchSkipRequest: handleEvent(matchSkipEvent), + packetIDs.client_matchScoreUpdate: handleEvent(matchFramesEvent), + packetIDs.client_matchComplete: handleEvent(matchCompleteEvent), + packetIDs.client_matchNoBeatmap: handleEvent(matchNoBeatmapEvent), + packetIDs.client_matchHasBeatmap: handleEvent(matchHasBeatmapEvent), + packetIDs.client_matchTransferHost: handleEvent(matchTransferHostEvent), + packetIDs.client_matchFailed: handleEvent(matchFailedEvent), + packetIDs.client_matchChangeTeam: handleEvent(matchChangeTeamEvent), + packetIDs.client_invite: handleEvent(matchInviteEvent), + } - # Token queue built, send it - responseTokenString = userToken.token - responseData = userToken.queue - userToken.resetQueue() + # Packets processed if in restricted mode. + # All other packets will be ignored if the user is in restricted mode + packetsRestricted = [ + packetIDs.client_logout, + packetIDs.client_userStatsRequest, + packetIDs.client_requestStatusUpdate, + packetIDs.client_userPanelRequest, + packetIDs.client_changeAction, + packetIDs.client_channelJoin, + packetIDs.client_channelPart, + ] - # Update ping time for timeout - userToken.updatePingTime() - except exceptions.tokenNotFoundException: - # Token not found. Disconnect that user - responseData = serverPackets.loginError() - responseData += serverPackets.notification("Whoops! Something went wrong, please login again.") - log.warning("Received packet from unknown token ({}).".format(requestTokenString)) - log.info("{} has been disconnected (invalid token)".format(requestTokenString)) - finally: - # Unlock token - if userToken != None: - userToken.lock.release() + # Process/ignore packet + if packetID != 4: + if packetID in eventHandler: + if userToken.restricted == False or (userToken.restricted == True and packetID in packetsRestricted): + eventHandler[packetID]() + else: + log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID)) + else: + log.warning("Unknown packet id from {} ({})".format(requestTokenString, packetID)) - if glob.outputRequestTime == True: - # End time - et = datetime.datetime.now() + # Update pos so we can read the next stacked packet + # +7 because we add packet ID bytes, unused byte and data length bytes + pos += dataLength+7 - # Total time: - tt = float((et.microsecond-st.microsecond)/1000) - log.debug("Request time: {}ms".format(tt)) + # Token queue built, send it + responseTokenString = userToken.token + responseData = userToken.queue + userToken.resetQueue() - # Send server's response to client - # We don't use token object because we might not have a token (failed login) - if glob.gzip == True: - # First, write the gzipped response - responseData = gzip.compress(responseData, int(glob.conf.config["server"]["gziplevel"])) + # Update ping time for timeout + userToken.updatePingTime() + except exceptions.tokenNotFoundException: + # Token not found. Disconnect that user + responseData = serverPackets.loginError() + responseData += serverPackets.notification("Whoops! Something went wrong, please login again.") + log.warning("Received packet from unknown token ({}).".format(requestTokenString)) + log.info("{} has been disconnected (invalid token)".format(requestTokenString)) + finally: + # Unlock token + if userToken != None: + userToken.lock.release() - # Then, add gzip headers - bottle.response.add_header("Vary", "Accept-Encoding") - bottle.response.add_header("Content-Encoding", "gzip") - else: - # First, write the response - responseData = responseData + if glob.outputRequestTime == True: + # End time + et = datetime.datetime.now() - # Add all the headers AFTER the response has been written - bottle.response.status = 200 - bottle.response.add_header("cho-token", responseTokenString) - bottle.response.add_header("cho-protocol", "19") - bottle.response.add_header("Content-Type", "text/html; charset=UTF-8") - yield responseData + # Total time: + tt = float((et.microsecond-st.microsecond)/1000) + log.debug("Request time: {}ms".format(tt)) -@bottle.route("/", method="GET") -def GETMain(): - html = "MA MAURO ESISTE?
"
-	html += "           _                 __
" - html += " (_) / /
" - html += " ______ __ ____ ____ / /____
" - html += " / ___/ / _ \\/ _ \\/ / _ \\
" - html += " / / / / /_) / /_) / / ____/
" - html += "/__/ /__/ .___/ .___/__/ \\_____/
" - html += " / / / /
" - html += " /__/ /__/
" - html += "PYTHON > ALL VERSION

" - html += "
" - html += " .. o .
" - html += " o.o o . o
" - html += " oo...
" - html += " __[]__
" - html += " phwr--> _\\:D/_/o_o_o_|__ u wot m8
" - html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/
" - html += " \\ . .. .. . /
" - html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
" - html += "

reverse engineering a protocol impossible to reverse engineer since always
we are actually reverse engineering bancho successfully. for the third time.

© Ripple team, 2016
" - yield html + # Send server's response to client + # We don't use token object because we might not have a token (failed login) + if glob.gzip == True: + # First, write the gzipped response + self.write(gzip.compress(responseData, int(glob.conf.config["server"]["gziplevel"]))) + + # Then, add gzip headers + self.add_header("Vary", "Accept-Encoding") + self.add_header("Content-Encoding", "gzip") + else: + # First, write the response + self.write(responseData) + + # Add all the headers AFTER the response has been written + self.set_status(200) + self.add_header("cho-token", responseTokenString) + self.add_header("cho-protocol", "19") + #self.add_header("Keep-Alive", "timeout=5, max=100") + #self.add_header("Connection", "keep-alive") + self.add_header("Content-Type", "text/html; charset=UTF-8") + except: + log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc())) + if glob.sentry: + yield tornado.gen.Task(self.captureException, exc_info=True) + #finally: + # self.finish() + + @tornado.web.asynchronous + @tornado.gen.engine + def asyncGet(self): + html = "MA MAURO ESISTE?
"
+		html += "           _                 __
" + html += " (_) / /
" + html += " ______ __ ____ ____ / /____
" + html += " / ___/ / _ \\/ _ \\/ / _ \\
" + html += " / / / / /_) / /_) / / ____/
" + html += "/__/ /__/ .___/ .___/__/ \\_____/
" + html += " / / / /
" + html += " /__/ /__/
" + html += "PYTHON > ALL VERSION

" + html += "
" + html += " .. o .
" + html += " o.o o . o
" + html += " oo...
" + html += " __[]__
" + html += " phwr--> _\\:D/_/o_o_o_|__ u wot m8
" + html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/
" + html += " \\ . .. .. . /
" + html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
" + html += "

reverse engineering a protocol impossible to reverse engineer since always
we are actually reverse engineering bancho successfully. for the third time.

© Ripple team, 2016
" + self.write(html) + #yield tornado.gen.Task(self.captureMessage, "test") + #self.finish() \ No newline at end of file diff --git a/helpers/requestHelper.py b/helpers/requestHelper.py index 0365631..b3defe6 100644 --- a/helpers/requestHelper.py +++ b/helpers/requestHelper.py @@ -1,7 +1,89 @@ +import tornado +import tornado.web +import tornado.gen +from tornado.ioloop import IOLoop from objects import glob +from raven.contrib.tornado import SentryMixin +from raven.contrib.tornado import AsyncSentryClient +import gevent -def getRequestIP(bottleRequest): - realIP = bottleRequest.headers.get("X-Forwarded-For") if glob.cloudflare == True else bottleRequest.headers.get("X-Real-IP") - if realIP != None: - return realIP - return bottleRequest.environ.get("REMOTE_ADDR") \ No newline at end of file +class asyncRequestHandler(tornado.web.RequestHandler): + """ + Tornado asynchronous request handler + create a class that extends this one (requestHelper.asyncRequestHandler) + use asyncGet() and asyncPost() instad of get() and post(). + Done. I'm not kidding. + """ + @tornado.web.asynchronous + @tornado.gen.engine + def get(self, *args, **kwargs): + try: + yield tornado.gen.Task(runBackground, (self.asyncGet, tuple(args), dict(kwargs))) + except Exception as e: + yield tornado.gen.Task(self.captureException, exc_info=True) + finally: + if not self._finished: + self.finish() + + @tornado.web.asynchronous + @tornado.gen.engine + def post(self, *args, **kwargs): + try: + yield tornado.gen.Task(runBackground, (self.asyncPost, tuple(args), dict(kwargs))) + except Exception as e: + yield tornado.gen.Task(self.captureException, exc_info=True) + finally: + if not self._finished: + self.finish() + + def asyncGet(self, *args, **kwargs): + self.send_error(405) + self.finish() + + def asyncPost(self, *args, **kwargs): + self.send_error(405) + self.finish() + + def getRequestIP(self): + realIP = self.request.headers.get("X-Forwarded-For") if glob.cloudflare == True else self.request.headers.get("X-Real-IP") + if realIP != None: + return realIP + return self.request.remote_ip + + +def runBackground(data, callback): + """ + Run a function in the background. + Used to handle multiple requests at the same time + """ + func, args, kwargs = data + def _callback(result): + IOLoop.instance().add_callback(lambda: callback(result)) + #glob.pool.apply_async(func, args, kwargs, _callback) + g = gevent.Greenlet(func, *args, **kwargs) + g.link(_callback) + g.start() + +def checkArguments(arguments, requiredArguments): + """ + Check that every requiredArguments elements are in arguments + + arguments -- full argument list, from tornado + requiredArguments -- required arguments list es: ["u", "ha"] + handler -- handler string name to print in exception. Optional + return -- True if all arguments are passed, none if not + """ + for i in requiredArguments: + if i not in arguments: + return False + return True + +def printArguments(t): + """ + Print passed arguments, for debug purposes + + t -- tornado object (self) + """ + print("ARGS::") + for i in t.request.arguments: + print ("{}={}".format(i, t.get_argument(i))) \ No newline at end of file diff --git a/pep.py b/pep.py index 95114bf..747f60e 100644 --- a/pep.py +++ b/pep.py @@ -3,11 +3,17 @@ import sys import os import threading -# Bottle -import bottle +# Tornado +import tornado.ioloop +import tornado.web +import tornado.httpserver +import tornado.gen from gevent import monkey as brit_monkey brit_monkey.patch_all() +# Raven +from raven.contrib.tornado import AsyncSentryClient + # pep.py files from constants import bcolors from helpers import configHelper @@ -20,13 +26,27 @@ from helpers import databaseHelperNew from helpers import generalFunctions from helpers import logHelper as log -# Raven -from raven import Client -from raven.contrib.bottle import Sentry +from handlers import mainHandler +from handlers import apiIsOnlineHandler +from handlers import apiOnlineUsersHandler +from handlers import apiServerStatusHandler +from handlers import ciTriggerHandler +from handlers import apiVerifiedStatusHandler +from handlers import apiFokabotMessageHandler -# IRC from irc import ircserver +def make_app(): + return tornado.web.Application([ + (r"/", mainHandler.handler), + (r"/api/v1/isOnline", apiIsOnlineHandler.handler), + (r"/api/v1/onlineUsers", apiOnlineUsersHandler.handler), + (r"/api/v1/serverStatus", apiServerStatusHandler.handler), + (r"/api/v1/ciTrigger", ciTriggerHandler.handler), + (r"/api/v1/verifiedStatus", apiVerifiedStatusHandler.handler), + (r"/api/v1/fokabotMessage", apiFokabotMessageHandler.handler) + ]) + if __name__ == "__main__": # Server start consoleHelper.printServerStartHeader(True) @@ -140,22 +160,13 @@ if __name__ == "__main__": consoleHelper.printColored("[!] Warning! Server running in debug mode!", bcolors.YELLOW) # Make app - app = bottle.app() - app.catchall = False - from handlers import mainHandler - from handlers import apiIsOnlineHandler - from handlers import apiOnlineUsersHandler - from handlers import apiServerStatusHandler - from handlers import ciTriggerHandler - from handlers import apiVerifiedStatusHandler - from handlers import apiFokabotMessageHandler + application = make_app() # Set up sentry try: glob.sentry = generalFunctions.stringToBool(glob.conf.config["sentry"]["enable"]) if glob.sentry == True: - client = Client(glob.conf.config["sentry"]["banchodns"], release=glob.VERSION) - app = Sentry(app, client) + application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodns"], release=glob.VERSION) else: consoleHelper.printColored("[!] Warning! Sentry logging is disabled!", bcolors.YELLOW) except: @@ -185,8 +196,9 @@ if __name__ == "__main__": consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED) # Server start message and console output - log.logMessage("Server started!", discord=True, stdout=False) - consoleHelper.printColored("> Bottle listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) + log.logMessage("Server started!", discord=True, of="info.txt", stdout=False) + consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) - # Start bottle - bottle.run(app=app, host="0.0.0.0", port=serverPort, server="gevent") + # Start tornado + application.listen(serverPort) + tornado.ioloop.IOLoop.instance().start() \ No newline at end of file