diff --git a/constants/exceptions.py b/constants/exceptions.py index e9967af..e353197 100644 --- a/constants/exceptions.py +++ b/constants/exceptions.py @@ -77,3 +77,6 @@ class need2FAException(Exception): class userRestrictedException(Exception): pass + +class haxException(Exception): + pass diff --git a/constants/fokabotCommands.py b/constants/fokabotCommands.py index 398b024..3fb1216 100644 --- a/constants/fokabotCommands.py +++ b/constants/fokabotCommands.py @@ -127,7 +127,7 @@ def kick(fro, chan, message): def fokabotReconnect(fro, chan, message): # Check if fokabot is already connected if glob.tokens.getTokenFromUserID(999) != None: - return"Fokabot is already connected to Bancho" + return "Fokabot is already connected to Bancho" # Fokabot is not connected, connect it fokabot.connect() diff --git a/constants/serverPackets.py b/constants/serverPackets.py index b1e0a0e..13ac0cf 100644 --- a/constants/serverPackets.py +++ b/constants/serverPackets.py @@ -130,16 +130,9 @@ def userStats(userID, force = False): # Get userID's token from tokens list userToken = glob.tokens.getTokenFromUserID(userID) if userToken == None: - return bytes() # NOTE: ??? - if userToken.restricted == True and force == False: return bytes() - # Stats are cached in token object - #rankedScore = userHelper.getRankedScore(userID, userToken.gameMode) - #accuracy = userHelper.getAccuracy(userID, userToken.gameMode)/100 - #playcount = userHelper.getPlaycount(userID, userToken.gameMode) - #totalScore = userHelper.getTotalScore(userID, userToken.gameMode) - #gameRank = userHelper.getGameRank(userID, userToken.gameMode) - #pp = int(userHelper.getPP(userID, userToken.gameMode)) + if (userToken.restricted == True or userToken.irc == True) and force == False: + return bytes() return packetHelper.buildPacket(packetIDs.server_userStats, [ [userID, dataTypes.uInt32], diff --git a/events/changeActionEvent.py b/events/changeActionEvent.py index 1c1dde0..dbf5e47 100644 --- a/events/changeActionEvent.py +++ b/events/changeActionEvent.py @@ -4,6 +4,7 @@ from constants import serverPackets from helpers import userHelper from helpers import logHelper as log from constants import actions +from helpers import chatHelper as chat def handle(userToken, packetData): # Get usertoken data @@ -58,7 +59,7 @@ def handle(userToken, packetData): # NOTE: Remove this when osu!direct will be fixed if userToken.actionID == actions.osuDirect and userToken.osuDirectAlert == False: userToken.osuDirectAlert = True - userToken.enqueue(serverPackets.sendMessage("FokaBot", userToken.username, "Sup! osu!direct works, but you'll need to update the switcher to have the Download button working. If you didn't update the switcher yet, please do!")) + chat.sendMessage("FokaBot", userToken.username, "Sup! osu!direct works, but you'll need to update the switcher to have the Download button working. If you didn't update the switcher yet, please do!") # Console output diff --git a/events/channelJoinEvent.py b/events/channelJoinEvent.py index fc14b4c..a0a2053 100644 --- a/events/channelJoinEvent.py +++ b/events/channelJoinEvent.py @@ -1,56 +1,7 @@ -""" -Event called when someone joins a channel -""" - from constants import clientPackets -from helpers import consoleHelper -from constants import bcolors -from constants import serverPackets -from objects import glob -from constants import exceptions -from helpers import logHelper as log +from helpers import chatHelper as chat def handle(userToken, packetData): # Channel join packet packetData = clientPackets.channelJoin(packetData) - joinChannel(userToken, packetData["channel"]) - -def joinChannel(userToken, channelName): - ''' - Join a channel - - userToken -- user token object of user that joins the chanlle - channelName -- name of channel - ''' - try: - # Get usertoken data - username = userToken.username - userID = userToken.userID - - # Check spectator channel - # If it's spectator channel, skip checks and list stuff - if channelName != "#spectator" and channelName != "#multiplayer": - # Normal channel, do check stuff - # Make sure the channel exists - if channelName not in glob.channels.channels: - raise exceptions.channelUnknownException - - # Check channel permissions - if glob.channels.channels[channelName].publicRead == False and userToken.admin == False: - raise exceptions.channelNoPermissionsException - - # Add our userID to users in that channel - glob.channels.channels[channelName].userJoin(userID) - - # Add the channel to our joined channel - userToken.joinChannel(channelName) - - # Send channel joined - userToken.enqueue(serverPackets.channelJoinSuccess(userID, channelName)) - - # Console output - log.info("{} joined channel {}".format(username, channelName)) - except exceptions.channelNoPermissionsException: - log.warning("{} attempted to join channel {}, but they have no read permissions".format(username, channelName)) - except exceptions.channelUnknownException: - log.warning("{} attempted to join an unknown channel ({})".format(username, channelName)) + chat.joinChannel(token=userToken, channel=packetData["channel"]) diff --git a/events/channelPartEvent.py b/events/channelPartEvent.py index 11d1ad3..845ed01 100644 --- a/events/channelPartEvent.py +++ b/events/channelPartEvent.py @@ -8,13 +8,14 @@ from objects import glob from constants import clientPackets from constants import serverPackets from helpers import logHelper as log +from helpers import chatHelper as chat def handle(userToken, packetData): - # Channel part packet + # Channel join packet packetData = clientPackets.channelPart(packetData) - partChannel(userToken, packetData["channel"]) + chat.partChannel(token=userToken, channel=packetData["channel"]) -def partChannel(userToken, channelName, kick = False): +"""def partChannel(userToken, channelName, kick = False): # Get usertoken data username = userToken.username userID = userToken.userID @@ -34,4 +35,4 @@ def partChannel(userToken, channelName, kick = False): userToken.enqueue(serverPackets.channelKicked(channelName)) # Console output - log.info("{} parted channel {}".format(username, channelName)) + log.info("{} parted channel {}".format(username, channelName))""" diff --git a/events/joinMatchEvent.py b/events/joinMatchEvent.py index 4ff0e42..6f75519 100644 --- a/events/joinMatchEvent.py +++ b/events/joinMatchEvent.py @@ -3,6 +3,7 @@ from constants import serverPackets from objects import glob from constants import exceptions from helpers import logHelper as log +from helpers import chatHelper as chat def handle(userToken, packetData): # read packet data @@ -44,8 +45,7 @@ def joinMatch(userToken, matchID, password): # Send packets userToken.enqueue(serverPackets.matchJoinSuccess(matchID)) - userToken.enqueue(serverPackets.channelJoinSuccess(userID, "#multiplayer")) - #userToken.enqueue(serverPackets.sendMessage("FokaBot", "#multiplayer", "Hi {}, and welcome to Ripple's multiplayer mode! This feature is still WIP and might have some issues. If you find any bugs, please report them (by clicking here)[https://ripple.moe/index.php?p=22].".format(username))) + chat.joinChannel(token=userToken, channel="#multi_{}".format(matchID)) except exceptions.matchNotFoundException: userToken.enqueue(serverPackets.matchJoinFail()) log.warning("{} has tried to join a mp room, but it doesn't exist".format(userToken.username)) diff --git a/events/loginEvent.py b/events/loginEvent.py index 7d83253..7115a52 100644 --- a/events/loginEvent.py +++ b/events/loginEvent.py @@ -8,12 +8,12 @@ from helpers import locationHelper from helpers import countryHelper import time from helpers import generalFunctions -from events import channelJoinEvent import sys import traceback from helpers import requestHelper from helpers import discordBotHelper from helpers import logHelper as log +from helpers import chatHelper as chat def handle(tornadoRequest): # Data to return @@ -30,6 +30,10 @@ def handle(tornadoRequest): # If true, print error to console err = False + # Make sure loginData is valid + if len(loginData) < 3: + raise exceptions.haxException() + # Try to get the ID from username username = str(loginData[0]) userID = userHelper.getID(username) @@ -59,6 +63,9 @@ def handle(tornadoRequest): responseToken = glob.tokens.addToken(userID, requestIP) responseTokenString = responseToken.token + # Check restricted mode (and eventually send message) + responseToken.checkRestricted() + # Set silence end UNIX time in token responseToken.silenceEndTime = userHelper.getSilenceEnd(userID) @@ -101,12 +108,12 @@ def handle(tornadoRequest): responseToken.enqueue(serverPackets.channelInfoEnd()) # Default opened channels # TODO: Configurable default channels - channelJoinEvent.joinChannel(responseToken, "#osu") - channelJoinEvent.joinChannel(responseToken, "#announce") + chat.joinChannel(token=responseToken, channel="#osu") + chat.joinChannel(token=responseToken, channel="#announce") # Join admin channel if we are an admin if responseToken.admin == True: - channelJoinEvent.joinChannel(responseToken, "#admin") + chat.joinChannel(token=responseToken, channel="#admin") # Output channels info for key, value in glob.channels.channels.items(): @@ -156,6 +163,12 @@ def handle(tornadoRequest): # (we don't use enqueue because we don't have a token since login has failed) err = True responseData += serverPackets.loginFailed() + except exceptions.haxException: + # Invalid POST data + # (we don't use enqueue because we don't have a token since login has failed) + err = True + responseData += serverPackets.loginFailed() + responseData += serverPackets.notification("I see what you're doing...") except exceptions.loginBannedException: # Login banned error packet err = True @@ -172,9 +185,14 @@ def handle(tornadoRequest): except exceptions.need2FAException: # User tried to log in from unknown IP responseData += serverPackets.needVerification() + except: + log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc())) finally: # Console and discord log - msg = "Bancho login request from {} for user {} ({})".format(requestIP, loginData[0], "failed" if err == True else "success") + 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) # Return token string and data diff --git a/events/logoutEvent.py b/events/logoutEvent.py index ef71beb..2666f57 100644 --- a/events/logoutEvent.py +++ b/events/logoutEvent.py @@ -4,8 +4,9 @@ from constants import bcolors from constants import serverPackets import time from helpers import logHelper as log +from helpers import chatHelper as chat -def handle(userToken, _): +def handle(userToken, _=None): # get usertoken data userID = userToken.userID username = userToken.username @@ -15,8 +16,9 @@ def handle(userToken, _): # the old logout packet will still be in the queue and will be sent to # the server, so we accept logout packets sent at least 5 seconds after login # if the user logs out before 5 seconds, he will be disconnected later with timeout check - if int(time.time()-userToken.loginTime) >= 5: + if int(time.time()-userToken.loginTime) >= 5 or userToken.irc == True: # Stop spectating if needed + # TODO: Call stopSpectatingEvent!!!!!!!!! if userToken.spectating != 0: # The user was spectating someone spectatorHostToken = glob.tokens.getTokenFromUserID(userToken.spectating) @@ -26,13 +28,17 @@ def handle(userToken, _): # Part all joined channels for i in userToken.joinedChannels: - glob.channels.channels[i].userPart(userID) + chat.partChannel(token=userToken, channel=i) # TODO: Lobby left if joined # Enqueue our disconnection to everyone else glob.tokens.enqueueAll(serverPackets.userLogout(userID)) + # Disconnect from IRC if needed + if userToken.irc == True and glob.irc == True: + glob.ircServer.forceDisconnection(userToken.username) + # Delete token glob.tokens.deleteToken(requestToken) diff --git a/events/partLobbyEvent.py b/events/partLobbyEvent.py index 768f32c..e66224e 100644 --- a/events/partLobbyEvent.py +++ b/events/partLobbyEvent.py @@ -1,6 +1,7 @@ from objects import glob from events import channelPartEvent from helpers import logHelper as log +from helpers import chatHelper as chat def handle(userToken, _): # Get usertoken data @@ -11,7 +12,7 @@ def handle(userToken, _): glob.matches.lobbyUserPart(userID) # Part lobby channel - channelPartEvent.partChannel(userToken, "#lobby", True) + chat.partChannel(channel="#lobby", token=userToken, kick=True) # Console output log.info("{} has left multiplayer lobby".format(username)) diff --git a/events/partMatchEvent.py b/events/partMatchEvent.py index e02036b..933c7ba 100644 --- a/events/partMatchEvent.py +++ b/events/partMatchEvent.py @@ -1,4 +1,5 @@ from objects import glob +from helpers import chatHelper as chat from constants import serverPackets def handle(userToken, _): @@ -22,6 +23,8 @@ def handle(userToken, _): # Set slot to free match.userLeft(userID) + # Part #multiplayer channel + chat.partChannel(token=userToken, channel="#multi_{}".format(matchID)) + # Set usertoken match to -1 userToken.partMatch() - userToken.enqueue(serverPackets.channelKicked("#multiplayer")) diff --git a/events/sendPrivateMessageEvent.py b/events/sendPrivateMessageEvent.py index 3e34a3e..98eee35 100644 --- a/events/sendPrivateMessageEvent.py +++ b/events/sendPrivateMessageEvent.py @@ -1,79 +1,7 @@ -from helpers import consoleHelper -from constants import bcolors from constants import clientPackets -from constants import serverPackets -from objects import glob -from objects import fokabot -from constants import exceptions -from constants import messageTemplates -from helpers import generalFunctions -from helpers import userHelper -from helpers import logHelper as log -import time +from helpers import chatHelper as chat def handle(userToken, packetData): - """ - Event called when someone sends a private message - - userToken -- request user token - packetData -- request data bytes - """ - - try: - # Get usertoken username - username = userToken.username - userID = userToken.userID - - # Make sure the user is not in restricted mode - if userToken.restricted == True: - raise exceptions.userRestrictedException - - # Private message packet - packetData = clientPackets.sendPrivateMessage(packetData) - - # Make sure the user is not silenced - if userToken.isSilenced() == True: - raise exceptions.userSilencedException - - # Check message length - packetData["message"] = packetData["message"][:2048]+"..." if len(packetData["message"]) > 2048 else packetData["message"] - - if packetData["to"] == "FokaBot": - # FokaBot command check - fokaMessage = fokabot.fokabotResponse(username, packetData["to"], packetData["message"]) - if fokaMessage != False: - userToken.enqueue(serverPackets.sendMessage("FokaBot", username, fokaMessage)) - log.pm("FokaBot -> {}: {}".format(packetData["to"], str(fokaMessage.encode("UTF-8")))) - else: - # Send packet message to target if it exists - token = glob.tokens.getTokenFromUsername(packetData["to"]) - if token == None: - raise exceptions.tokenNotFoundException() - - # Check message templates (mods/admins only) - if packetData["message"] in messageTemplates.templates and userToken.admin == True: - packetData["message"] = messageTemplates.templates[packetData["message"]] - - # Send message to target - token.enqueue(serverPackets.sendMessage(username, packetData["to"], packetData["message"])) - - # Send away message to sender if needed - if token.awayMessage != "": - userToken.enqueue(serverPackets.sendMessage(packetData["to"], username, "This user is away: {}".format(token.awayMessage))) - - # Spam protection - userToken.spamProtection() - - # Console and file output - log.pm("{} -> {}: {}".format(username, packetData["to"], packetData["message"])) - except exceptions.userSilencedException: - userToken.enqueue(serverPackets.silenceEndTime(userToken.getSilenceSecondsLeft())) - log.warning("{} tried to send a message during silence".format(username)) - except exceptions.tokenNotFoundException: - # Token not found, user disconnected - log.warning("{} tried to send a message to {}, but their token couldn't be found".format(username, packetData["to"])) - except exceptions.messageTooLongException: - # Message > 256 silence - userToken.silence(2*3600, "Sending messages longer than 256 characters") - except exceptions.userRestrictedException: - pass + # Send private message packet + packetData = clientPackets.sendPrivateMessage(packetData) + chat.sendMessage(token=userToken, to=packetData["to"], message=packetData["message"]) diff --git a/events/sendPublicMessageEvent.py b/events/sendPublicMessageEvent.py index 926c1bd..86956dd 100644 --- a/events/sendPublicMessageEvent.py +++ b/events/sendPublicMessageEvent.py @@ -1,135 +1,7 @@ -from constants import exceptions from constants import clientPackets -from objects import glob -from objects import fokabot -from constants import serverPackets -from helpers import discordBotHelper -from helpers import logHelper as log -from helpers import userHelper -import time +from helpers import chatHelper as chat def handle(userToken, packetData): - """ - Event called when someone sends a public message - - userToken -- request user token - packetData -- request data bytes - """ - - try: - # Get userToken data - userID = userToken.userID - username = userToken.username - - # Make sure the user is not in restricted mode - if userToken.restricted == True: - raise exceptions.userRestrictedException - - # Public chat packet - packetData = clientPackets.sendPublicMessage(packetData) - - # Receivers - who = [] - - # Make sure the user is not silenced - if userToken.isSilenced() == True: - raise exceptions.userSilencedException - - # Check message length - packetData["message"] = packetData["message"][:2048]+"..." if len(packetData["message"]) > 2048 else packetData["message"] - - # Get receivers list - # Check #spectator - if packetData["to"] == "#spectator": - # Spectator channel - # Send this packet to every spectator and host - if userToken.spectating == 0: - # We have sent to send a message to our #spectator channel - targetToken = userToken - who = targetToken.spectators[:] - # No need to remove us because we are the host so we are not in spectators list - else: - # We have sent a message to someone else's #spectator - targetToken = glob.tokens.getTokenFromUserID(userToken.spectating) - who = targetToken.spectators[:] - - # Remove us - if userID in who: - who.remove(userID) - - # Add host - who.append(targetToken.userID) - elif packetData["to"] == "#multiplayer": - # Multiplayer Channel - # Get match ID and match object - matchID = userToken.matchID - - # Make sure we are in a match - if matchID == -1: - return - - # Make sure the match exists - if matchID not in glob.matches.matches: - return - - # The match exists, get object - match = glob.matches.matches[matchID] - - # Create targets list - who = [] - for i in range(0,16): - uid = match.slots[i]["userID"] - if uid > -1 and uid != userID: - who.append(uid) - else: - # Standard channel - # Make sure the channel exists - if packetData["to"] not in glob.channels.channels: - raise exceptions.channelUnknownException - - # Make sure the channel is not in moderated mode - if glob.channels.channels[packetData["to"]].moderated == True and userToken.admin == False: - raise exceptions.channelModeratedException - - # Make sure we have write permissions - if glob.channels.channels[packetData["to"]].publicWrite == False and userToken.admin == False: - raise exceptions.channelNoPermissionsException - - # Send this packet to everyone in that channel except us - who = glob.channels.channels[packetData["to"]].getConnectedUsers()[:] - if userID in who: - who.remove(userID) - - # We have receivers - # Send packet to required users - glob.tokens.multipleEnqueue(serverPackets.sendMessage(username, packetData["to"], packetData["message"]), who, False) - - # Fokabot command check - fokaMessage = fokabot.fokabotResponse(username, packetData["to"], packetData["message"]) - if fokaMessage != False: - who.append(userID) - glob.tokens.multipleEnqueue(serverPackets.sendMessage("FokaBot", packetData["to"], fokaMessage), who, False) - log.chat("FokaBot @ {}: {}".format(packetData["to"], str(fokaMessage.encode("UTF-8")))) - - # Spam protection - userToken.spamProtection() - - # Console and file log - log.chat("{fro} @ {to}: {message}".format(fro=username, to=packetData["to"], message=str(packetData["message"].encode("utf-8")))) - - # Discord log - discordBotHelper.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=packetData["to"], message=str(packetData["message"].encode("utf-8"))[2:-1])) - except exceptions.userSilencedException: - userToken.enqueue(serverPackets.silenceEndTime(userToken.getSilenceSecondsLeft())) - log.warning("{} tried to send a message during silence".format(username)) - except exceptions.channelModeratedException: - log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(username, packetData["to"])) - except exceptions.channelUnknownException: - log.warning("{} tried to send a message to an unknown channel ({})".format(username, packetData["to"])) - except exceptions.channelNoPermissionsException: - log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(username, packetData["to"])) - except exceptions.messageTooLongException: - # Message > 256 silence - userToken.silence(2*3600, "Sending messages longer than 256 characters") - except exceptions.userRestrictedException: - pass + # Send public message packet + packetData = clientPackets.sendPublicMessage(packetData) + chat.sendMessage(token=userToken, to=packetData["to"], message=packetData["message"]) diff --git a/events/startSpectatingEvent.py b/events/startSpectatingEvent.py index 2eeec23..9bbddcb 100644 --- a/events/startSpectatingEvent.py +++ b/events/startSpectatingEvent.py @@ -4,6 +4,7 @@ from constants import exceptions from objects import glob from helpers import userHelper from helpers import logHelper as log +from helpers import chatHelper as chat def handle(userToken, packetData): try: @@ -34,12 +35,12 @@ def handle(userToken, packetData): # Send spectator join packet to host targetToken.enqueue(serverPackets.addSpectator(userID)) - # Join #spectator channel - userToken.enqueue(serverPackets.channelJoinSuccess(userID, "#spectator")) - + # Create and join #spectator (#spect_userid) channel + glob.channels.addTempChannel("#spect_{}".format(targetToken.userID)) + chat.joinChannel(token=userToken, channel="#spect_{}".format(targetToken.userID)) if len(targetToken.spectators) == 1: # First spectator, send #spectator join to host too - targetToken.enqueue(serverPackets.channelJoinSuccess(userID, "#spectator")) + chat.joinChannel(token=targetToken, channel="#spect_{}".format(targetToken.userID)) # send fellowSpectatorJoined to all spectators for spec in targetToken.spectators: diff --git a/events/stopSpectatingEvent.py b/events/stopSpectatingEvent.py index 9161714..1332425 100644 --- a/events/stopSpectatingEvent.py +++ b/events/stopSpectatingEvent.py @@ -2,6 +2,7 @@ from objects import glob from constants import serverPackets from constants import exceptions from helpers import logHelper as log +from helpers import chatHelper as chat def handle(userToken, _): try: @@ -16,6 +17,9 @@ def handle(userToken, _): raise exceptions.tokenNotFoundException targetToken.removeSpectator(userID) + # Part #spectator channel + chat.partChannel(token=userToken, channel="#spect_{}".format(target)) + # Send the spectator left packet to host targetToken.enqueue(serverPackets.removeSpectator(userID)) for c in targetToken.spectators: @@ -25,8 +29,7 @@ def handle(userToken, _): #targetToken.enqueue(serverPackets.fellowSpectatorLeft(userID)) # Console output - # TODO: Move messages in stop spectating - log.info("{} are no longer spectating whoever they were spectating".format(username)) + log.info("{} are no longer spectating {}".format(username, target)) except exceptions.tokenNotFoundException: log.warning("Spectator stop: token not found") finally: diff --git a/handlers/mainHandler.py b/handlers/mainHandler.py index f02aa8b..0c6853b 100644 --- a/handlers/mainHandler.py +++ b/handlers/mainHandler.py @@ -1,5 +1,7 @@ import datetime import gzip +import time +from helpers import generalFunctions from helpers import requestHelper from objects import glob from helpers import consoleHelper @@ -257,7 +259,7 @@ class handler(SentryMixin, requestHelper.asyncRequestHandler): 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." + 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() diff --git a/helpers/chatHelper.py b/helpers/chatHelper.py new file mode 100644 index 0000000..9f5f2bd --- /dev/null +++ b/helpers/chatHelper.py @@ -0,0 +1,335 @@ +from objects import glob +from helpers import logHelper as log +from constants import exceptions +from constants import serverPackets +from objects import fokabot +from helpers import discordBotHelper +from helpers import userHelper +from events import logoutEvent +from events import channelJoinEvent +from constants import messageTemplates + +def joinChannel(userID = 0, channel = "", token = None, toIRC = True): + """ + Join a channel + + userID -- user ID of the user that joins the channel. Optional. + token can be used instead. + token -- user token object of user that joins the channel. Optional. + userID can be used instead. + channel -- name of channe + toIRC -- if True, send this channel join event to IRC. Must be true if joining from bancho. + Optional. Defaukt: True + return -- returns 0 if joined or other IRC code in case of error. Needed only on IRC-side + """ + try: + # Get token if not defined + if token == None: + token = glob.tokens.getTokenFromUserID(userID) + # Make sure the token exists + if token == None: + raise exceptions.userNotFoundException + else: + token = token + userID = token.userID + + # Get usertoken data + username = token.username + + # Normal channel, do check stuff + # Make sure the channel exists + if channel not in glob.channels.channels: + raise exceptions.channelUnknownException + + # Check channel permissions + channelObject = glob.channels.channels[channel] + if channelObject.publicRead == False and token.admin == False: + raise exceptions.channelNoPermissionsException + + # Add our userID to users in that channel + channelObject.userJoin(userID) + + # Add the channel to our joined channel + token.joinChannel(channel) + + # Send channel joined (bancho). We use clientName here because of #multiplayer and #spectator channels + token.enqueue(serverPackets.channelJoinSuccess(userID, channelObject.clientName)) + + # Send channel joined (IRC) + if glob.irc == True and toIRC == True: + glob.ircServer.banchoJoinChannel(username, channel) + + # Console output + log.info("{} joined channel {}".format(username, channel)) + + # IRC code return + return 0 + except exceptions.channelNoPermissionsException: + log.warning("{} attempted to join channel {}, but they have no read permissions".format(username, channel)) + return 403 + except exceptions.channelUnknownException: + log.warning("{} attempted to join an unknown channel ({})".format(username, channel)) + return 403 + except exceptions.userNotFoundException: + log.warning("User not connected to IRC/Bancho") + return 403 # idk + +def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = False): + """ + Part a channel + + userID -- user ID of the user that parts the channel. Optional. + token can be used instead. + token -- user token object of user that parts the channel. Optional. + userID can be used instead. + channel -- name of channel + toIRC -- if True, send this channel join event to IRC. Must be true if joining from bancho. + Optional. Defaukt: True + kick -- if True, channel tab will be closed on client. Used when leaving lobby. Optional. Default: False + return -- returns 0 if joined or other IRC code in case of error. Needed only on IRC-side + """ + try: + # Get token if not defined + if token == None: + token = glob.tokens.getTokenFromUserID(userID) + # Make sure the token exists + if token == None: + raise exceptions.userNotFoundException + else: + token = token + userID = token.userID + + # Get usertoken data + username = token.username + + # Determine internal/client name if needed + # (toclient is used clientwise for #multiplayer and #spectator channels) + channelClient = channel + if channel == "#spectator": + if token.spectating == 0: + s = userID + else: + s = token.spectating + channel = "#spect_{}".format(s) + elif channel == "#multiplayer": + channel = "#multi_{}".format(token.matchID) + elif channel.startswith("#spect_"): + channelClient = "#spectator" + elif channel.startswith("#multi_"): + channelClient = "#multiplayer" + + # Make sure the channel exists + if channel not in glob.channels.channels: + raise exceptions.channelUnknownException + + # Part channel (token-side and channel-side) + channelObject = glob.channels.channels[channel] + token.partChannel(channel) + channelObject.userPart(userID) + + # Force close tab if needed + # NOTE: Maybe always needed, will check later + if kick == True: + token.enqueue(serverPackets.channelKicked(channelObject.clientName)) + + # IRC part + if glob.irc == True and toIRC == True: + glob.ircServer.banchoPartChannel(username, channel) + + # Console output + log.info("{} parted channel {}".format(username, channel)) + + # Return IRC code + return 0 + except exceptions.channelUnknownException: + log.warning("{} attempted to part an unknown channel ({})".format(username, channel)) + return 403 + except exceptions.userNotFoundException: + log.warning("User not connected to IRC/Bancho") + return 442 # idk + + + + +def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True): + """ + Send a message to osu!bancho and IRC server + + fro -- sender username. Optional. + You can use token instead of this if you wish. + to -- receiver channel (if starts with #) or username + message -- text of the message + token -- sender token object. + You can use this instead of fro if you are sending messages from bancho. + Optional. + toIRC -- if True, send the message to IRC. If False, send it to Bancho only. + Optional. Default: True + """ + try: + tokenString = "" + # Get token object if not passed + if token == None: + token = glob.tokens.getTokenFromUsername(fro) + if token == None: + raise exceptions.userNotFoundException + else: + # token object alredy passed, get its string and its username (fro) + fro = token.username + tokenString = token.token + + # Set some variables + userID = token.userID + username = token.username + recipients = [] + + # Make sure the user is not in restricted mode + if token.restricted == True: + raise exceptions.userRestrictedException + + # Make sure the user is not silenced + if token.isSilenced() == True: + raise exceptions.userSilencedException + + # Determine internal name if needed + # (toclient is used clientwise for #multiplayer and #spectator channels) + toClient = to + if to == "#spectator": + if token.spectating == 0: + s = userID + else: + s = token.spectating + to = "#spect_{}".format(s) + elif to == "#multiplayer": + to = "#multi_{}".format(token.matchID) + elif to.startswith("#spect_"): + toClient = "#spectator" + elif to.startswith("#multi_"): + toClient = "#multiplayer" + + # Truncate message if > 2048 characters + message = message[:2048]+"..." if len(message) > 2048 else message + + # Build packet bytes + packet = serverPackets.sendMessage(username, toClient, message) + + # Send the message + isChannel = to.startswith("#") + if isChannel == True: + # CHANNEL + # Make sure the channel exists + if to not in glob.channels.channels: + raise exceptions.channelUnknownException + + # Make sure the channel is not in moderated mode + if glob.channels.channels[to].moderated == True and token.admin == False: + raise exceptions.channelModeratedException + + # Make sure we have write permissions + if glob.channels.channels[to].publicWrite == False and token.admin == False: + raise exceptions.channelNoPermissionsException + + # Everything seems fine, build recipients list and send packet + recipients = glob.channels.channels[to].getConnectedUsers()[:] + for key, value in glob.tokens.tokens.items(): + # Skip our client and irc clients + if key == tokenString or value.irc == True: + continue + # Send to this client if it's connected to the channel + if value.userID in recipients: + value.enqueue(packet) + else: + # USER + # Make sure recipient user is connected + recipientToken = glob.tokens.getTokenFromUsername(to) + if recipientToken == None: + raise exceptions.userNotFoundException + + # Make sure the recipient is not restricted or we are FokaBot + if recipientToken.restricted == True and fro.lower() != "fokabot": + raise exceptions.userRestrictedException + + # TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend + + # Check message templates (mods/admins only) + if message in messageTemplates.templates and token.admin == True: + sendMessage(fro, to, messageTemplates.templates[message]) + + # Everything seems fine, send packet + recipientToken.enqueue(packet) + + # Send the message to IRC + if glob.irc == True and toIRC == True: + glob.ircServer.banchoMessage(fro, to, message) + + # Spam protection (ignore FokaBot) + if userID > 999: + token.spamProtection() + + # Fokabot message + if isChannel == True or to.lower() == "fokabot": + fokaMessage = fokabot.fokabotResponse(username, to, message) + if fokaMessage != False: + sendMessage("FokaBot", to if isChannel else fro, fokaMessage) + + # File and discord logs (public chat only) + if to.startswith("#") == True: + log.chat("{fro} @ {to}: {message}".format(fro=username, to=to, message=str(message.encode("utf-8")))) + discordBotHelper.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))[2:-1])) + return 0 + except exceptions.userSilencedException: + token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft())) + log.warning("{} tried to send a message during silence".format(username)) + return 404 + except exceptions.channelModeratedException: + log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(username, to)) + return 404 + except exceptions.channelUnknownException: + log.warning("{} tried to send a message to an unknown channel ({})".format(username, to)) + return 403 + except exceptions.channelNoPermissionsException: + log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(username, to)) + return 404 + except exceptions.userRestrictedException: + log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(username, to)) + return 404 + except exceptions.userNotFoundException: + log.warning("User not connected to IRC/Bancho") + return 401 + + + +""" IRC-Bancho Connect/Disconnect/Join/Part interfaces""" +def IRCConnect(username): + userID = userHelper.getID(username) + if userID == False: + log.warning("{} doesn't exist".format(username)) + return + glob.tokens.deleteOldTokens(userID) + glob.tokens.addToken(userID, irc=True) + glob.tokens.enqueueAll(serverPackets.userPanel(userID)) + log.info("{} logged in from IRC".format(username)) + +def IRCDisconnect(username): + token = glob.tokens.getTokenFromUsername(username) + if token == None: + log.warning("{} doesn't exist".format(username)) + return + logoutEvent.handle(token) + log.info("{} disconnected from IRC".format(username)) + +def IRCJoinChannel(username, channel): + userID = userHelper.getID(username) + if userID == False: + log.warning("{} doesn't exist".format(username)) + return + # NOTE: This should have also `toIRC` = False` tho, + # since we send JOIN message later on ircserver.py. + # Will test this later + return joinChannel(userID, channel) + +def IRCPartChannel(username, channel): + userID = userHelper.getID(username) + if userID == False: + log.warning("{} doesn't exist".format(username)) + return + return partChannel(userID, channel) diff --git a/helpers/configHelper.py b/helpers/configHelper.py index 1fc8a5f..f02a728 100644 --- a/helpers/configHelper.py +++ b/helpers/configHelper.py @@ -52,7 +52,6 @@ class config: self.config.get("server","threads") self.config.get("server","gzip") self.config.get("server","gziplevel") - self.config.get("server","localize") self.config.get("server","cikey") self.config.get("server","cloudflare") @@ -66,6 +65,12 @@ class config: self.config.get("discord","enable") self.config.get("discord","boturl") self.config.get("discord","devgroup") + + self.config.get("irc","enable") + self.config.get("irc","port") + + self.config.get("localize","enable") + self.config.get("localize","ipapiurl") return True except: return False @@ -91,7 +96,6 @@ class config: self.config.set("server", "threads", "16") self.config.set("server", "gzip", "1") self.config.set("server", "gziplevel", "6") - self.config.set("server", "localize", "1") self.config.set("server", "cikey", "changeme") self.config.set("server", "cloudflare", "0") @@ -109,6 +113,14 @@ class config: self.config.set("discord", "boturl", "") self.config.set("discord", "devgroup", "") + self.config.add_section("irc") + self.config.set("irc", "enable", "1") + self.config.set("irc", "port", "6667") + + self.config.add_section("localize") + self.config.set("localize", "enable", "1") + self.config.set("localize", "ipapiurl", "http://ip.zxq.co") + # Write ini to file and close self.config.write(f) f.close() diff --git a/helpers/databaseHelperNew.py b/helpers/databaseHelperNew.py index 5768601..51dfa0c 100644 --- a/helpers/databaseHelperNew.py +++ b/helpers/databaseHelperNew.py @@ -44,7 +44,7 @@ class db: self.lastWorker = 0 self.workersNumber = workers for i in range(0,self.workersNumber): - print("> Spawning MySQL pettirosso meme {}".format(i)) + print(".", end="") self.workers.append(mysqlWorker(i, host, username, password, database)) def getWorker(self): diff --git a/helpers/generalFunctions.py b/helpers/generalFunctions.py index 98ecf13..9e56e26 100644 --- a/helpers/generalFunctions.py +++ b/helpers/generalFunctions.py @@ -21,7 +21,7 @@ def hexString(s): return -- string with hex value """ - return ":".join("{:02x}".format(ord(c)) for c in s) + return ":".join("{:02x}".format(ord(str(c))) for c in s) def readableMods(__mods): """ diff --git a/helpers/locationHelper.py b/helpers/locationHelper.py index 250dbec..1220aa6 100644 --- a/helpers/locationHelper.py +++ b/helpers/locationHelper.py @@ -1,12 +1,9 @@ import urllib.request import json +from objects import glob from helpers import logHelper as log -# API URL -URL = "http://ip.zxq.co/" - - def getCountry(ip): """ Get country from IP address @@ -17,7 +14,7 @@ def getCountry(ip): try: # Try to get country from Pikolo Aul's Go-Sanic ip API - result = json.loads(urllib.request.urlopen("{}/{}".format(URL, ip), timeout=3).read().decode())["country"] + result = json.loads(urllib.request.urlopen("{}/{}".format(glob.conf.config["localize"]["ipapiurl"], ip), timeout=3).read().decode())["country"] return result.upper() except: log.error("Error in get country") @@ -34,7 +31,7 @@ def getLocation(ip): try: # Try to get position from Pikolo Aul's Go-Sanic ip API - result = json.loads(urllib.request.urlopen("{}/{}".format(URL, ip), timeout=3).read().decode())["loc"].split(",") + result = json.loads(urllib.request.urlopen("{}/{}".format(glob.conf.config["localize"]["ipapiurl"], ip), timeout=3).read().decode())["loc"].split(",") return [float(result[0]), float(result[1])] except: log.error("Error in get position") diff --git a/helpers/packetHelper.py b/helpers/packetHelper.py index d701de5..8cb79ec 100644 --- a/helpers/packetHelper.py +++ b/helpers/packetHelper.py @@ -118,7 +118,7 @@ def packData(__data, __dataType): # Non empty string data += b"\x0B" data += uleb128Encode(len(__data)) - data += str.encode(__data, "latin_1") + data += str.encode(__data, "latin_1", "ignore") elif __dataType == dataTypes.uInt16: packType = " {}".format(self.ip, self.port, data)) + quitmsg = "EOT" + except socket.error as x: + # Error while reading data, this client will be disconnected + data = "" + quitmsg = x + + if data: + # Parse received data if needed + self.__readbuffer += data.decode("latin_1") + self.parseBuffer() + self.__timestamp = time.time() + self.__sentPing = False + else: + # No data, disconnect this socket + self.disconnect(quitmsg) + + + def parseBuffer(self): + """Parse self.__readbuffer, get command, arguments and call its handler""" + # Get lines from buffer + lines = self.__linesep_regexp.split(self.__readbuffer) + self.__readbuffer = lines[-1] + lines = lines[:-1] + + # Process every line + for line in lines: + if not line: + # Empty line. Ignore. + continue + + # Get arguments + x = line.split(" ", 1) + + # Command is the first argument, always uppercase + command = x[0].upper() + + if len(x) == 1: + # Command only, no arguments + arguments = [] + else: + # We have some arguments + # Weird sorcery + if len(x[1]) > 0 and x[1][0] == ":": + arguments = [x[1][1:]] + else: + y = x[1].split(" :", 1) + arguments = y[0].split() + if len(y) == 2: + arguments.append(y[1]) + + # Handle command with its arguments + self.__handleCommand(command, arguments) + + + def writeSocket(self): + """Write buffer to socket""" + try: + sent = self.socket.send(self.__writebuffer.encode()) + log.debug("[IRC] [{}:{}] <- {}".format(self.ip, self.port, self.__writebuffer[:sent])) + self.__writebuffer = self.__writebuffer[sent:] + except socket.error as x: + self.disconnect(x) + + + def checkAlive(self): + """Check if this client is still connected""" + now = time.time() + if self.__timestamp + 180 < now: + self.disconnect("ping timeout") + return + if not self.__sentPing and self.__timestamp + 90 < now: + if self.__handleCommand == self.mainHandler: + # Registered. + self.message("PING :{}".format(self.server.host)) + self.__sentPing = True + else: + # Not registered. + self.disconnect("ping timeout") + + + def sendLusers(self): + """Send lusers response to this client""" + self.replyCode(251, "There are {} users and 0 services on 1 server".format(len(glob.tokens.tokens))) + + def sendMotd(self): + """Send MOTD to this client""" + self.replyCode(375, "- {} Message of the day - ".format(self.server.host)) + if len(self.server.motd) == 0: + self.replyCode(422, "MOTD File is missing") + else: + for i in self.server.motd: + self.replyCode(372, "- {}".format(i)) + self.replyCode(376, "End of MOTD command") + + """"""""" + HANDLERS + """"""""" + def dummyHandler(self, command, arguments): + pass + + def passHandler(self, command, arguments): + """PASS command handler""" + if command == "PASS": + if len(arguments) == 0: + self.reply461("PASS") + else: + # IRC token check + m = hashlib.md5() + m.update(arguments[0].encode("utf-8")) + tokenHash = m.hexdigest() + supposedUsername = glob.db.fetch("SELECT users.username FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1", [tokenHash]) + if supposedUsername: + self.supposedUsername = supposedUsername["username"] + self.__handleCommand = self.registerHandler + else: + # Wrong IRC Token + self.reply("464 :Password incorrect") + elif command == "QUIT": + self.disconnect() + + + def registerHandler(self, command, arguments): + """NICK and USER commands handler""" + if command == "NICK": + if len(arguments) < 1: + self.reply("431 :No nickname given") + return + nick = arguments[0] + + # Make sure this is the first time we set our nickname + if self.username != "": + self.reply("432 * %s :Erroneous nickname" % nick) + return + + # Make sure the IRC token was correct: + if nick.lower() != self.supposedUsername.lower(): + self.reply("464 :Password incorrect") + return + + # Make sure we are not connected to Bancho + token = glob.tokens.getTokenFromUsername(nick) + if token != None: + self.reply("433 * {} :Nickname is already in use".format(nick)) + return + + # Make sure we are not already connected from IRC with that name + for _, value in self.server.clients.items(): + if value.username == self.username and value != self: + self.reply("433 * {} :Nickname is already in use".format(nick)) + return + + # Everything seems fine, set username (nickname) + self.username = nick + elif command == "USER": + # Ignore USER command, we use nickname only + return + elif command == "QUIT": + # Disconnect if we have received a QUIT command + self.disconnect() + return + else: + # Ignore any other command while logging in + return + + # If we now have a valid username, connect to bancho and send IRC welcome stuff + if self.username != "": + # Bancho connection + chat.IRCConnect(self.username) + + # IRC reply + self.replyCode(1, "Welcome to the Internet Relay Network") + self.replyCode(2, "Your host is {}, running version pep.py-{}".format(self.server.host, glob.VERSION)) + self.replyCode(3, "This server was created since the beginning") + self.replyCode(4, "{} pep.py-{} o o".format(self.server.host, glob.VERSION)) + self.sendLusers() + self.sendMotd() + self.__handleCommand = self.mainHandler + + def quitHandler(self, command, arguments): + """QUIT command handler""" + self.disconnect(self.username if len(arguments) < 1 else arguments[0]) + + def joinHandler(self, command, arguments): + """JOIN command handler""" + if len(arguments) < 1: + self.reply461("JOIN") + return + + # Get bancho token object + token = glob.tokens.getTokenFromUsername(self.username) + if token == None: + return + + # TODO: Part all channels + if arguments[0] == "0": + return + '''for (channelname, channel) in self.channels.items(): + self.message_channel(channel, "PART", channelname, True) + self.channel_log(channel, "left", meta=True) + server.remove_member_from_channel(self, channelname) + self.channels = {} + return''' + + # Get channels to join list + channels = arguments[0].split(",") + + for channel in channels: + # Make sure we are not already in that channel + # (we already check this bancho-side, but we need to do it + # also here k maron) + if channel.lower() in token.joinedChannels: + continue + + # Attempt to join the channel + response = chat.IRCJoinChannel(self.username, channel) + if response == 0: + # Joined successfully + self.joinedChannels.append(channel) + + # Let everyone in this channel know that we've joined + self.messageChannel(channel, "{} JOIN".format(self.username), channel, True) + + # Send channel description (topic) + description = glob.channels.channels[channel].description + if description == "": + self.replyCode(331, "No topic is set", channel=channel) + else: + self.replyCode(332, description, channel=channel) + + # Build connected users list + users = glob.channels.channels[channel].getConnectedUsers()[:] + usernames = [] + for user in users: + token = glob.tokens.getTokenFromUserID(user) + if token == None: + continue + usernames.append(token.username) + usernames = " ".join(usernames) + + # Send IRC users lis + self.replyCode(353, usernames, channel="= {}".format(channel)) + self.replyCode(366, "End of NAMES list", channel=channel) + elif response == 403: + # Channel doesn't exist (or no read permissions) + self.reply403(channel) + continue + + def partHandler(self, command, arguments): + """PART command handler""" + if len(arguments) < 1: + self.reply461("PART") + return + + # Get bancho token object + token = glob.tokens.getTokenFromUsername(self.username) + if token == None: + return + + # Get channels to part list + channels = arguments[0].split(",") + + for channel in channels: + # Make sure we in that channel + # (we already check this bancho-side, but we need to do it + # also here k maron) + if channel.lower() not in token.joinedChannels: + continue + + # Attempt to part the channel + response = chat.IRCPartChannel(self.username, channel) + if response == 0: + # No errors, remove channel from joinedChannels + self.joinedChannels.remove(channel) + elif response == 403: + self.reply403(channel) + elif response == 442: + self.replyCode(442, "You're not on that channel", channel=channel) + + + + def noticePrivmsgHandler(self, command, arguments): + """NOTICE and PRIVMSG commands handler (same syntax)""" + # Syntax check + if len(arguments) == 0: + self.replyCode(411, "No recipient given ({})".format(command)) + return + if len(arguments) == 1: + self.replyCode(412, "No text to send") + return + recipient = arguments[0] + message = arguments[1] + + # Send the message to bancho and reply + response = chat.sendMessage(self.username, recipient, message, toIRC=False) + if response == 404: + self.replyCode(404, "Cannot send to channel", channel=recipient) + return + elif response == 403: + self.replyCode(403, "No such channel", channel=recipient) + return + elif response == 401: + self.replyCode(401, "No such nick/channel", channel=recipient) + return + + # Send the message to IRC and bancho + if recipient.startswith("#"): + # Public message (IRC) + if recipient not in glob.channels.channels: + self.replyCode(401, "No such nick/channel", channel=recipient) + return + for _, value in self.server.clients.items(): + if recipient in value.joinedChannels and value != self: + value.message(":{} PRIVMSG {} :{}".format(self.username, recipient, message)) + #self.messageChannel(recipient, command, "{} :{}".format(recipient, message)) + else: + # Private message (IRC) + for _, value in self.server.clients.items(): + if value.username == recipient: + value.message(":{} PRIVMSG {} :{}".format(self.username, recipient, message)) + + def motdHandler(self, command, arguments): + """MOTD command handler""" + self.sendMotd() + + def lusersHandler(self, command, arguments): + """LUSERS command handler""" + self.sendLusers() + + def pingHandler(self, command, arguments): + """PING command handler""" + if len(arguments) < 1: + self.replyCode(409, "No origin specified") + return + self.reply("PONG {} :{}".format(self.server.host, arguments[0])) + + def pongHandler(self, command, arguments): + """(fake) PONG command handler""" + pass + + def mainHandler(self, command, arguments): + """Handler for post-login commands""" + handlers = { + #"AWAY": away_handler, + #"ISON": ison_handler, + "JOIN": self.joinHandler, + #"LIST": list_handler, + "LUSERS": self.lusersHandler, + #"MODE": mode_handler, + "MOTD": self.motdHandler, + #"NICK": nick_handler, + #"NOTICE": notice_and_privmsg_handler, + "PART": self.partHandler, + "PING": self.pingHandler, + "PONG": self.pongHandler, + "PRIVMSG": self.noticePrivmsgHandler, + "QUIT": self.quitHandler, + #"TOPIC": topic_handler, + #"WALLOPS": wallops_handler, + #"WHO": who_handler, + #"WHOIS": whois_handler, + "USER": self.dummyHandler, + } + try: + handlers[command](command, arguments) + except KeyError: + self.replyCode(421, "Unknown command ({})".format(command)) + + + + + + + +class Server: + def __init__(self, port): + self.host = socket.getfqdn("127.0.0.1")[:63] + self.port = port + self.clients = {} # Socket --> Client instance. + self.motd = ["Welcome to pep.py's embedded IRC server!", "This is a VERY simple IRC server and it's still in beta.", "Expect things to crash and not work as expected :("] + + def forceDisconnection(self, username): + """ + Disconnect someone from IRC if connected + + username -- victim + """ + for _, value in self.clients.items(): + if value.username == username: + value.disconnect(callLogout=False) + break# or dictionary changes size during iteration + + def banchoJoinChannel(self, username, channel): + """ + Let every IRC client connected to a specific client know that 'username' joined the channel from bancho + + username -- username of bancho user + channel -- joined channel name + """ + for _, value in self.clients.items(): + if channel in value.joinedChannels: + value.message(":{} JOIN {}".format(username, channel)) + + def banchoPartChannel(self, username, channel): + """ + Let every IRC client connected to a specific client know that 'username' parted the channel from bancho + + username -- username of bancho user + channel -- joined channel name + """ + for _, value in self.clients.items(): + if channel in value.joinedChannels: + value.message(":{} PART {}".format(username, channel)) + + def banchoMessage(self, fro, to, message): + """ + Send a message to IRC when someone sends it from bancho + + fro -- sender username + to -- receiver username + message -- text of the message + """ + if to.startswith("#"): + # Public message + for _, value in self.clients.items(): + if to in value.joinedChannels and value.username != fro: + value.message(":{} PRIVMSG {} :{}".format(fro, to, message)) + else: + # Private message + for _, value in self.clients.items(): + if value.username == to and value.username != fro: + value.message(":{} PRIVMSG {} :{}".format(fro, to, message)) + + + def removeClient(self, client, quitmsg): + """ + Remove a client from connected clients + + client -- client object + quitmsg -- QUIT argument, useless atm + """ + if client.socket in self.clients: + del self.clients[client.socket] + + def start(self): + """Start IRC server main loop""" + serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + serversocket.bind(("127.0.0.1", self.port)) + except socket.error as e: + log.error("[IRC] Could not bind port {}:{}".format(self.port, e)) + sys.exit(1) + serversocket.listen(5) + lastAliveCheck = time.time() + + # Main server loop + while True: + (iwtd, owtd, ewtd) = select.select( + [serversocket] + [x.socket for x in self.clients.values()], + [x.socket for x in self.clients.values() + if x.writeBufferSize() > 0], + [], + 2) + + # Handle incoming connections + for x in iwtd: + if x in self.clients: + self.clients[x].readSocket() + else: + (conn, addr) = x.accept() + try: + self.clients[conn] = Client(self, conn) + log.info("[IRC] Accepted connection from {}:{}".format(addr[0], addr[1])) + except socket.error as e: + try: + conn.close() + except: + pass + + # Handle outgoing connections + for x in owtd: + if x in self.clients: # client may have been disconnected + self.clients[x].writeSocket() + + # Make sure all IRC clients are still connected + now = time.time() + if lastAliveCheck + 10 < now: + for client in list(self.clients.values()): + client.checkAlive() + lastAliveCheck = now + + +def main(port=6667): + glob.ircServer = Server(port) + glob.ircServer.start() diff --git a/objects/channel.py b/objects/channel.py index 607e1d8..70db719 100644 --- a/objects/channel.py +++ b/objects/channel.py @@ -1,3 +1,5 @@ +from objects import glob + class channel: """ A chat channel @@ -10,15 +12,7 @@ class channel: moderated -- bool """ - name = "" - description = "" - connectedUsers = [] - - publicRead = False - publicWrite = False - moderated = False - - def __init__(self, __name, __description, __publicRead, __publicWrite): + def __init__(self, __name, __description, __publicRead, __publicWrite, temp): """ Create a new chat channel object @@ -26,13 +20,23 @@ class channel: __description -- channel description __publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins __publicWrite -- bool, same as public read but relative to write permissions + temp -- if True, channel will be deleted when there's no one in the channel. Optional. Default = False. """ self.name = __name self.description = __description self.publicRead = __publicRead self.publicWrite = __publicWrite - self.connectedUsers = [] + self.moderated = False + self.temp = temp + self.connectedUsers = [999] # Fokabot is always connected to every channels (otherwise it doesn't show up in IRC users list) + + # Client name (#spectator/#multiplayer) + self.clientName = self.name + if self.name.startswith("#spect_"): + self.clientName = "#spectator" + elif self.name.startswith("#multi_"): + self.clientName = "#multiplayer" def userJoin(self, __userID): @@ -53,9 +57,13 @@ class channel: __userID -- user ID that left the channel """ - connectedUsers = self.connectedUsers - if __userID in connectedUsers: - connectedUsers.remove(__userID) + if __userID in self.connectedUsers: + self.connectedUsers.remove(__userID) + + # Remove temp channels if empty or there's only fokabot connected + l = len(self.connectedUsers) + if self.temp == True and ((l == 0) or (l == 1 and 999 in self.connectedUsers)): + glob.channels.removeChannel(self.name) def getConnectedUsers(self): @@ -64,7 +72,6 @@ class channel: return -- connectedUsers list """ - return self.connectedUsers @@ -74,5 +81,4 @@ class channel: return -- connected users number """ - return len(self.connectedUsers) diff --git a/objects/channelList.py b/objects/channelList.py index 9d91583..f6780a3 100644 --- a/objects/channelList.py +++ b/objects/channelList.py @@ -1,5 +1,6 @@ from objects import glob from objects import channel +from helpers import logHelper as log class channelList: """ @@ -27,14 +28,40 @@ class channelList: self.addChannel(i["name"], i["description"], publicRead, publicWrite) - def addChannel(self, __name, __description, __publicRead, __publicWrite): + def addChannel(self, name, description, publicRead, publicWrite, temp = False): """ Add a channel object to channels dictionary - __name -- channel name - __description -- channel description - __publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins - __publicWrite -- bool, same as public read but relative to write permissions + name -- channel name + description -- channel description + publicRead -- bool, if true channel can be read by everyone, if false it can be read only by mods/admins + publicWrite -- bool, same as public read but relative to write permissions + temp -- if True, channel will be deleted when there's no one in the channel. Optional. Default = False. """ + self.channels[name] = channel.channel(name, description, publicRead, publicWrite, temp) + log.info("Created channel {}".format(name)) - self.channels[__name] = channel.channel(__name, __description, __publicRead, __publicWrite) + + def addTempChannel(self, name): + """ + Add a temporary channel (like #spectator or #multiplayer), gets deleted when there's no one in the channel + + name -- channel name + return -- True if channel was created, False if failed + """ + if name in self.channels: + return False + self.channels[name] = channel.channel(name, "Chat", True, True, True) + log.info("Created temp channel {}".format(name)) + + def removeChannel(self, name): + """ + Removes a channel from channels list + + name -- channel name + """ + if name not in self.channels: + log.debug("{} is not in channels list".format(name)) + return + self.channels.pop(name) + log.info("Removed channel {}".format(name)) diff --git a/objects/glob.py b/objects/glob.py index 03faa37..71423c6 100644 --- a/objects/glob.py +++ b/objects/glob.py @@ -9,6 +9,8 @@ from raven import Client try: with open("version") as f: VERSION = f.read() + if VERSION == "": + raise except: VERSION = "¯\_(xd)_/¯" diff --git a/objects/match.py b/objects/match.py index b3d7ec8..f5543ad 100644 --- a/objects/match.py +++ b/objects/match.py @@ -9,6 +9,7 @@ from constants import serverPackets from constants import dataTypes from constants import matchTeams from helpers import logHelper as log +from helpers import chatHelper as chat class match: """Multiplayer match object""" @@ -61,6 +62,9 @@ class match: for _ in range(0,16): self.slots.append({"status": slotStatuses.free, "team": 0, "userID": -1, "mods": 0, "loaded": False, "skip": False, "complete": False}) + # Create #multiplayer channel + glob.channels.addTempChannel("#multi_{}".format(self.matchID)) + def getMatchData(self): """ @@ -577,11 +581,11 @@ class match: # FokaBot is too busy if to == 999: - froToken.enqueue(serverPackets.sendMessage("FokaBot", froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.")) + chat.sendMessage("FokaBot", froToken.username, "I would love to join your match, but I'm busy keeping ripple up and running. Sorry. Beep Boop.") # Send message message = "Come join my multiplayer match: \"[osump://{}/{} {}]\"".format(self.matchID, self.matchPassword.replace(" ", "_"), self.matchName) - toToken.enqueue(serverPackets.sendMessage(froToken.username, toToken.username, message)) + chat.sendMessage(token=froToken, to=toToken.username, message=message) def countUsers(self): diff --git a/objects/osuToken.py b/objects/osuToken.py index 4d613c2..e022477 100644 --- a/objects/osuToken.py +++ b/objects/osuToken.py @@ -9,6 +9,7 @@ import uuid import time import threading from helpers import logHelper as log +from helpers import chatHelper as chat class token: """ @@ -34,7 +35,7 @@ class token: """ - def __init__(self, __userID, token = None, ip = ""): + def __init__(self, __userID, token = None, ip = "", irc = False): """ Create a token object and set userID and token @@ -42,6 +43,7 @@ class token: token -- if passed, set token to that value if not passed, token will be generated ip -- client ip. optional. + irc -- if True, set this token as IRC client. optional. """ # Set stuff @@ -49,6 +51,7 @@ class token: self.username = userHelper.getUsername(self.userID) self.privileges = userHelper.getPrivileges(self.userID) self.admin = userHelper.isInPrivilegeGroup(self.userID, "developer") or userHelper.isInPrivilegeGroup(self.userID, "community manager") + self.irc = irc self.restricted = userHelper.isRestricted(self.userID) self.loginTime = int(time.time()) self.pingTime = self.loginTime @@ -78,7 +81,6 @@ class token: self.actionMd5 = "" self.actionMods = 0 self.gameMode = gameModes.std - self.rankedScore = 0 self.accuracy = 0.0 self.playcount = 0 @@ -100,8 +102,9 @@ class token: userHelper.saveBanchoSession(self.userID, self.ip) # If we are restricted, send message from FokaBot to user - if self.restricted == True: - self.setRestricted() + # NOTE: Sent later + #if self.restricted == True: + # self.setRestricted() def enqueue(self, __bytes): @@ -110,8 +113,8 @@ class token: __bytes -- (packet) bytes to enqueue """ - - self.queue += __bytes + if self.irc == False: + self.queue += __bytes def resetQueue(self): @@ -228,11 +231,11 @@ class token: """Set match to -1""" self.matchID = -1 - def kick(self): + def kick(self, message="You have been kicked from the server. Please login again."): """Kick this user from the server""" # Send packet to target log.info("{} has been disconnected. (kick)".format(self.username)) - self.enqueue(serverPackets.notification("You have been kicked from the server. Please login again.")) + self.enqueue(serverPackets.notification(message)) self.enqueue(serverPackets.loginFailed()) # Logout event @@ -301,10 +304,22 @@ class token: self.gameRank = stats["gameRank"] self.pp = stats["pp"] + def checkRestricted(self, force=False): + """ + Check if this token is restricted. If so, send fokabot message + + force -- If True, get restricted value from db. + If false, get the cached one. Optional. Default: False + """ + if force == True: + self.restricted = userHelper.isRestricted(self.userID) + if self.restricted == True: + self.setRestricted() + def setRestricted(self): """ Set this token as restricted, send FokaBot message to user and send offline packet to everyone """ self.restricted = True - self.enqueue(serverPackets.sendMessage("FokaBot", self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.")) + chat.sendMessage("FokaBot",self.username, "Your account is currently in restricted mode. Please visit ripple's website for more information.") diff --git a/objects/tokenList.py b/objects/tokenList.py index a64fc76..6239cf4 100644 --- a/objects/tokenList.py +++ b/objects/tokenList.py @@ -19,15 +19,16 @@ class tokenList: """ self.tokens = {} - def addToken(self, userID, ip = ""): + def addToken(self, userID, ip = "", irc = False): """ Add a token object to tokens list userID -- user id associated to that token + irc -- if True, set this token as IRC client return -- token object """ - newToken = osuToken.token(userID, ip=ip) + newToken = osuToken.token(userID, ip=ip, irc=irc) self.tokens[newToken.token] = newToken return newToken @@ -40,7 +41,8 @@ class tokenList: if token in self.tokens: # Delete session from DB - userHelper.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip) + if self.tokens[token].ip != "": + userHelper.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip) # Pop token from list self.tokens.pop(token) @@ -108,13 +110,14 @@ class tokenList: """ # Delete older tokens - for key, value in self.tokens.items(): + for key, value in list(self.tokens.items()): if value.userID == userID: # Delete this token from the dictionary - self.tokens.pop(key) + self.tokens[key].kick("You have logged in from somewhere else. You can't connect to Bancho/IRC from more than one device at the same time.") + #self.tokens.pop(key) # break or items() function throws errors - break + #break def multipleEnqueue(self, packet, who, but = False): @@ -136,8 +139,6 @@ class tokenList: if shouldEnqueue: value.enqueue(packet) - - def enqueueAll(self, packet): """ Enqueue packet(s) to every connected user @@ -162,7 +163,7 @@ class tokenList: timeoutLimit = time.time()-__timeoutTime for key, value in self.tokens.items(): # Check timeout (fokabot is ignored) - if value.pingTime < timeoutLimit and value.userID != 999: + if value.pingTime < timeoutLimit and value.userID != 999 and value.irc == False: # That user has timed out, add to disconnected tokens # We can't delete it while iterating or items() throws an error timedOutTokens.append(key) @@ -195,3 +196,18 @@ class tokenList: Call at bancho startup to delete old cached sessions """ glob.db.execute("TRUNCATE TABLE bancho_sessions") + + def tokenExists(self, username = "", userID = -1): + """ + Check if a token exists (aka check if someone is connected) + + username -- Optional. + userID -- Optional. + return -- True if it exists, otherwise False + + Use username or userid, not both at the same time. + """ + if userID > -1: + return True if self.getTokenFromUserID(userID) is not None else False + else: + return True if self.getTokenFromUsername(username) is not None else False diff --git a/pep.py b/pep.py index f7ecffc..b50c2f4 100644 --- a/pep.py +++ b/pep.py @@ -2,6 +2,7 @@ import sys import os from multiprocessing.pool import ThreadPool +import threading # Tornado import tornado.ioloop @@ -30,6 +31,7 @@ from handlers import apiOnlineUsersHandler from handlers import apiServerStatusHandler from handlers import ciTriggerHandler +from irc import ircserver def make_app(): return tornado.web.Application([ @@ -67,8 +69,9 @@ if __name__ == "__main__": # Connect to db try: - print("> Connecting to MySQL db... ") + consoleHelper.printNoNl("> Connecting to MySQL db") glob.db = databaseHelperNew.db(glob.conf.config["db"]["host"], glob.conf.config["db"]["username"], glob.conf.config["db"]["password"], glob.conf.config["db"]["database"], int(glob.conf.config["db"]["workers"])) + consoleHelper.printNoNl(" ") consoleHelper.printDone() except: # Exception while connecting to db @@ -109,7 +112,7 @@ if __name__ == "__main__": consoleHelper.printDone() # Initialize chat channels - consoleHelper.printNoNl("> Initializing chat channels... ") + print("> Initializing chat channels... ") glob.channels.loadChannels() consoleHelper.printDone() @@ -129,7 +132,7 @@ if __name__ == "__main__": consoleHelper.printDone() # Localize warning - glob.localize = generalFunctions.stringToBool(glob.conf.config["server"]["localize"]) + glob.localize = generalFunctions.stringToBool(glob.conf.config["localize"]["enable"]) if glob.localize == False: consoleHelper.printColored("[!] Warning! Users localization is disabled!", bcolors.YELLOW) @@ -151,6 +154,20 @@ if __name__ == "__main__": if glob.debug == True: consoleHelper.printColored("[!] Warning! Server running in debug mode!", bcolors.YELLOW) + # IRC start message and console output + glob.irc = generalFunctions.stringToBool(glob.conf.config["irc"]["enable"]) + if glob.irc == True: + # IRC port + try: + ircPort = int(glob.conf.config["irc"]["port"]) + except: + consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED) + log.logMessage("IRC server started!", discord=True, of="info.txt", stdout=False) + consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN) + threading.Thread(target=lambda: ircserver.main(port=ircPort)).start() + else: + consoleHelper.printColored("[!] Warning! IRC server is disabled!", bcolors.YELLOW) + # Server port try: serverPort = int(glob.conf.config["server"]["port"]) @@ -158,7 +175,6 @@ if __name__ == "__main__": consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED) # Make app - #application = tornado.httpserver.HTTPServer(make_app()) application = make_app() # Set up sentry @@ -176,7 +192,7 @@ if __name__ == "__main__": # Server start message and console output log.logMessage("Server started!", discord=True, of="info.txt", stdout=False) - consoleHelper.printColored("> Tornado listening for clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) + consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN) # Start tornado application.listen(serverPort) diff --git a/version b/version index 88c5fb8..9c6d629 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.4.0 +1.6.1