47 Commits

Author SHA1 Message Date
Nyo
c6417c31ed ⬆️ v1.11.2 ⬆️ 2017-01-06 11:48:59 +01:00
Nyo
9055fcaf5e .BANCHO. .FIX. Fix some format errors 2016-12-28 16:15:07 +01:00
Nyo
f85640ae39 .HIDE. Update landscape config file 2016-12-28 16:12:39 +01:00
Nyo
18a7c47db6 .BANCHO. Add tornado-sentry capture decorators 2016-12-28 12:41:24 +01:00
Nyo
22ae4c332b .BANCHO. Log username changes to user notes in RAP 2016-12-28 12:16:46 +01:00
Nyo
8f156a0702 .HIDE. Update submodules 2016-12-26 10:33:39 +01:00
Nyo
62b67da9fb .HIDE. General refactoring 2016-12-26 10:33:05 +01:00
Nyo
501130721d .HIDE. Update submodules 2016-12-26 10:00:38 +01:00
Nyo
e6cdef4580 .BANCHO. Removed some schiavo logs 2016-12-26 09:59:33 +01:00
Nyo
00c544b7c7 .BANCHO. Removed some schiavo logs 2016-12-26 09:52:43 +01:00
Nyo
ebf0e1d458 .BANCHO. .FIX. Fix stop spectating not working properly if host disconnects 2016-12-21 23:26:47 +01:00
Nyo
fd23cf2b2c ⬆️ v1.11.1 ⬆️ 2016-12-21 18:20:55 +01:00
Nyo
8a8a4968a3 .BANCHO. .FIX. Fix streams and temporary chat channels not being disposed correctly 2016-12-21 18:17:29 +01:00
Nyo
2ae3c5f701 .BANCHO. Add redis notifications pubsub handler 2016-12-20 21:22:25 +01:00
Nyo
f4c099c809 .BANCHO. .FIX. Remove extra characters from version 2016-12-20 20:39:29 +01:00
Nyo
f8cc0c738c Add code health badge in README 2016-12-17 15:33:12 +01:00
Nyo
4557b08df8 Add landscape config file 2016-12-17 15:28:55 +01:00
Nyo
49f8bd8cf1 .BANCHO. .HIDE. Antiamo a kuartare i kantieri koi vekki............... 2016-12-12 22:59:58 +01:00
Nyo
7ba5db62b4 .BANCHO. Report result is now a notification rather than a FokaBot message 2016-12-12 22:57:00 +01:00
Nyo
7f534f0984 .BANCHO. .FIX. Fix hour in user report chatlog 2016-12-12 22:45:31 +01:00
Nyo
daf457fc5c .BANCHO. Add !report command 2016-12-11 23:12:06 +01:00
Nyo
b4d498c26c .BANCHO. Kick all clients when using !kick, don't kick FokaBot with !kick command 2016-12-11 11:39:01 +01:00
Nyo
44545c3bcb .BANCHO. Use streams for public chat 2016-12-11 11:07:35 +01:00
Nyo
c4a6c84cec .BANCHO. .HIDE. General refactoring 2016-12-10 19:31:12 +01:00
Nyo
8532731f19 .BANCHO. .FIX. Run safeUsername on /api/v1/isOnline 2016-12-10 19:30:38 +01:00
Nyo
b836f77446 .BANCHO. .FIX. Fix !kick command not working on irc clients 2016-12-10 19:10:00 +01:00
Nyo
e92cbe47bd ⬆️ 1.11.0 ⬆️ 2016-12-09 13:19:04 +01:00
Nyo
6ca2016f7b .BANCHO. Disabled datadog ram usage tracking 2016-12-09 11:47:00 +01:00
Nyo
2f54a56b7a Add full build script 2016-12-08 15:46:21 +01:00
Nyo
cf9e506875 .HIDE. Update submodules 2016-12-08 12:04:59 +01:00
Nyo
5c93d692ea .BANCHO. Cythonized mainHandler 2016-12-08 11:44:27 +01:00
Nyo
a8a1dfb1bc .HIDE. Update .gitignore 2016-12-08 11:44:12 +01:00
Nyo
9d562e7acd .BANCHO. Dynamic setup.py file 2016-12-08 11:43:23 +01:00
Nyo
4f4253afce .HIDE. Update README.txt 2016-12-07 22:31:28 +01:00
Nyo
04898c24ae .BANCHO. Ported packet encoder/decoder to Cython, add distutils setup file, update .gitignore, README and requirements.txt 2016-12-07 22:25:16 +01:00
Nyo
1b94936092 .BANCHO. .FIX. Fix wrong default configuration file 2016-12-07 21:15:55 +01:00
Nyo
d4591b42a3 .BANCHO. !kickall command now requires 'manage server' privilege 2016-12-07 21:15:23 +01:00
Nyo
69508f9a0e Add Google auth 2fa check at login 2016-11-30 23:33:56 +01:00
Nyo
5cf8c1bde8 Merge branch 'master' of git.zxq.co:ripple/pep.py 2016-11-30 20:08:54 +01:00
Nyo
20be60d9db Update submodules 2016-11-30 20:08:43 +01:00
Howl
61935f323c add link to github mirror 2016-11-29 17:21:24 +01:00
Nyo
cecef18d13 .HIDE. Update submodules 2016-11-20 14:17:35 +01:00
Nyo
5723c0e68f .BANCHO. Move online users count to redis 2016-11-20 14:17:05 +01:00
Nyo
525235a27e .BANCHO. Move bancho sessions to redis 2016-11-20 13:03:07 +01:00
Nyo
3bc390e3e6 .HIDE. Update submodules 2016-11-20 12:32:45 +01:00
Nyo
f6ae673401 .HIDE. Update submodules 2016-11-20 11:32:21 +01:00
Nyo
aa32e8bea6 .BANCHO. Add pubsub handlers for username changes, bans, restrictions, silences, stats update, kicks and bancho settings reload. 2016-11-20 11:31:51 +01:00
57 changed files with 1001 additions and 565 deletions

6
.gitignore vendored
View File

@@ -1,9 +1,9 @@
**/__pycache__ **/__pycache__
**/build
config.ini config.ini
filters.txt filters.txt
.data .data
.idea .idea
common_funzia
common_refractor
common_memato
redistest.py redistest.py
*.c
*.so

7
.landscape.yaml Normal file
View File

@@ -0,0 +1,7 @@
python-targets:
- 3
pep8:
none: true
pylint:
disable:
- cyclic-import

View File

@@ -1,4 +1,8 @@
## pep.py ## pep.py [![Code Health](https://landscape.io/github/osuripple/pep.py/master/landscape.svg?style=flat)](https://landscape.io/github/osuripple/pep.py/master)
- Origin: https://git.zxq.co/ripple/pep.py
- Mirror: https://github.com/osuripple/pep.py
This is Ripple's bancho server. It handles: This is Ripple's bancho server. It handles:
- Client login - Client login
- Online users listing and statuses - Online users listing and statuses
@@ -9,6 +13,8 @@ This is Ripple's bancho server. It handles:
## Requirements ## Requirements
- Python 3.5 - Python 3.5
- Cython
- C compiler
- MySQLdb (`mysqlclient`) - MySQLdb (`mysqlclient`)
- Tornado - Tornado
- Bcrypt - Bcrypt
@@ -23,9 +29,14 @@ afterwards, install the required dependencies with pip
``` ```
$ pip install -r requirements.txt $ pip install -r requirements.txt
``` ```
then, run pep.py once to create the default config file and edit it then, compile all `*.pyx` files to `*.so` or `*.dll` files using `setup.py` (distutils file)
```
$ python3 setup.py build_ext --inplace
```
finally, run pep.py once to create the default config file and edit it
``` ```
$ python3 pep.py $ python3 pep.py
...
$ nano config.ini $ nano config.ini
``` ```
you can run pep.py by typing you can run pep.py by typing

2
common

Submodule common updated: db36e8d589...6329b9ac2d

View File

@@ -89,4 +89,16 @@ class unknownStreamException(Exception):
pass pass
class userTournamentException(Exception): class userTournamentException(Exception):
pass
class userAlreadyInChannelException(Exception):
pass
class userNotInChannelException(Exception):
pass
class missingReportInfoException(Exception):
pass
class invalidUserException(Exception):
pass pass

View File

@@ -1,7 +1,9 @@
import json import json
import random import random
import re
import requests import requests
import time
from common import generalUtils from common import generalUtils
from common.constants import mods from common.constants import mods
@@ -14,6 +16,7 @@ from constants import serverPackets
from helpers import systemHelper from helpers import systemHelper
from objects import fokabot from objects import fokabot
from objects import glob from objects import glob
from helpers import chatHelper as chat
""" """
Commands callbacks Commands callbacks
@@ -119,15 +122,18 @@ def kickAll(fro, chan, message):
def kick(fro, chan, message): def kick(fro, chan, message):
# Get parameters # Get parameters
target = message[0].replace("_", " ") target = message[0].lower().replace("_", " ")
if target == "fokabot":
return "Nope."
# Get target token and make sure is connected # Get target token and make sure is connected
targetToken = glob.tokens.getTokenFromUsername(target) tokens = glob.tokens.getTokenFromUsername(target, _all=True)
if targetToken is None: if len(tokens) == 0:
return "{} is not online".format(target) return "{} is not online".format(target)
# Kick user # Kick users
targetToken.kick() for i in tokens:
i.kick()
# Bot response # Bot response
return "{} has been kicked from the server.".format(target) return "{} has been kicked from the server.".format(target)
@@ -304,22 +310,7 @@ def systemShutdown(fro, chan, message):
return restartShutdown(False) return restartShutdown(False)
def systemReload(fro, chan, message): def systemReload(fro, chan, message):
# Reload settings from bancho_settings glob.banchoConf.reload()
glob.banchoConf.loadSettings()
# Reload channels too
glob.channels.loadChannels()
# And chat filters
glob.chatFilters.loadFilters()
# Send new channels and new bottom icon to everyone
glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
glob.streams.broadcast("main", serverPackets.channelInfoEnd())
for key, value in glob.channels.channels.items():
if value.publicRead == True and value.hidden == False:
glob.streams.broadcast("main", serverPackets.channelInfo(key))
return "Bancho settings reloaded!" return "Bancho settings reloaded!"
def systemMaintenance(fro, chan, message): def systemMaintenance(fro, chan, message):
@@ -395,7 +386,7 @@ def getPPMessage(userID, just_data = False):
currentAcc = token.tillerino[2] currentAcc = token.tillerino[2]
# Send request to LETS api # Send request to LETS api
resp = requests.get("http://127.0.0.1:5002/api/v1/pp?b={}&m={}".format(currentMap, currentMods, currentAcc), timeout=10).text resp = requests.get("http://127.0.0.1:5002/api/v1/pp?b={}&m={}".format(currentMap, currentMods), timeout=10).text
data = json.loads(resp) data = json.loads(resp)
# Make sure status is in response data # Make sure status is in response data
@@ -671,7 +662,7 @@ def pp(fro, chan, message):
pp = userUtils.getPP(token.userID, gameMode) pp = userUtils.getPP(token.userID, gameMode)
return "You have {:,} pp".format(pp) return "You have {:,} pp".format(pp)
def updateBeatmap(fro, chan, to): def updateBeatmap(fro, chan, message):
try: try:
# Run the command in PM only # Run the command in PM only
if chan.startswith("#"): if chan.startswith("#"):
@@ -706,6 +697,69 @@ def updateBeatmap(fro, chan, to):
except: except:
return False return False
def report(fro, chan, message):
msg = ""
try:
# TODO: Rate limit
# Regex on message
reportRegex = re.compile("^(.+) \((.+)\)\:(?: )?(.+)?$")
result = reportRegex.search(" ".join(message))
# Make sure the message matches the regex
if result is None:
raise exceptions.invalidArgumentsException()
# Get username, report reason and report info
target, reason, additionalInfo = result.groups()
target = chat.fixUsernameForBancho(target)
# Make sure the target is not foka
if target.lower() == "fokabot":
raise exceptions.invalidUserException()
# Make sure the user exists
targetID = userUtils.getID(target)
if targetID == 0:
raise exceptions.userNotFoundException()
# Make sure that the user has specified additional info if report reason is 'Other'
if reason.lower() == "other" and additionalInfo is None:
raise exceptions.missingReportInfoException()
# Get the token if possible
chatlog = ""
token = glob.tokens.getTokenFromUsername(target)
if token is not None:
chatlog = token.getMessagesBufferString()
# Everything is fine, submit report
glob.db.execute("INSERT INTO reports (id, from_uid, to_uid, reason, chatlog, time) VALUES (NULL, %s, %s, %s, %s, %s)", [userUtils.getID(fro), targetID, "{reason} - ingame {info}".format(reason=reason, info="({})".format(additionalInfo) if additionalInfo is not None else ""), chatlog, int(time.time())])
msg = "You've reported {target} for {reason}{info}. A Community Manager will check your report as soon as possible. Every !report message you may see in chat wasn't sent to anyone, so nobody in chat, but admins, know about your report. Thank you for reporting!".format(target=target, reason=reason, info="" if additionalInfo is None else " (" + additionalInfo + ")")
adminMsg = "{user} has reported {target} for {reason} ({info})".format(user=fro, target=target, reason=reason, info=additionalInfo)
# Log report in #admin and on discord
chat.sendMessage("FokaBot", "#admin", adminMsg)
log.warning(adminMsg, discord="cm")
except exceptions.invalidUserException:
msg = "Hello, FokaBot here! You can't report me. I won't forget what you've tried to do. Watch out."
except exceptions.invalidArgumentsException:
msg = "Invalid report command syntax. To report an user, click on it and select 'Report user'."
except exceptions.userNotFoundException:
msg = "The user you've tried to report doesn't exist."
except exceptions.missingReportInfoException:
msg = "Please specify the reason of your report."
except:
raise
finally:
if msg != "":
token = glob.tokens.getTokenFromUsername(fro)
if token is not None:
if token.irc:
chat.sendMessage("FokaBot", fro, msg)
else:
token.enqueue(serverPackets.notification(msg))
return False
""" """
Commands list Commands list
@@ -725,7 +779,7 @@ commands = [
"callback": faq "callback": faq
}, { }, {
"trigger": "!report", "trigger": "!report",
"response": "Report command isn't here yet :c" "callback": report
}, { }, {
"trigger": "!help", "trigger": "!help",
"response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for FokaBot's full command list" "response": "Click (here)[https://ripple.moe/index.php?p=16&id=4] for FokaBot's full command list"
@@ -753,7 +807,7 @@ commands = [
"callback": moderated "callback": moderated
}, { }, {
"trigger": "!kickall", "trigger": "!kickall",
"privileges": privileges.ADMIN_KICK_USERS, "privileges": privileges.ADMIN_MANAGE_SERVERS,
"callback": kickAll "callback": kickAll
}, { }, {
"trigger": "!kick", "trigger": "!kick",

View File

@@ -155,11 +155,13 @@ def channelJoinSuccess(userID, chan):
return packetHelper.buildPacket(packetIDs.server_channelJoinSuccess, [[chan, dataTypes.STRING]]) return packetHelper.buildPacket(packetIDs.server_channelJoinSuccess, [[chan, dataTypes.STRING]])
def channelInfo(chan): def channelInfo(chan):
if chan not in glob.channels.channels:
return bytes()
channel = glob.channels.channels[chan] channel = glob.channels.channels[chan]
return packetHelper.buildPacket(packetIDs.server_channelInfo, [ return packetHelper.buildPacket(packetIDs.server_channelInfo, [
[chan, dataTypes.STRING], [channel.name, dataTypes.STRING],
[channel.description, dataTypes.STRING], [channel.description, dataTypes.STRING],
[len(channel.connectedUsers), dataTypes.UINT16] [len(glob.streams.streams["chat/{}".format(chan)].clients), dataTypes.UINT16]
]) ])
def channelInfoEnd(): def channelInfoEnd():

View File

@@ -1,24 +1,21 @@
from common.constants import actions
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils
from constants import clientPackets from constants import clientPackets
from constants import serverPackets from constants import serverPackets
from objects import glob from objects import glob
def handle(userToken, packetData): def handle(userToken, packetData):
# Get usertoken data # Get usertoken data
userID = userToken.userID userID = userToken.userID
username = userToken.username username = userToken.username
# Make sure we are not banned # Make sure we are not banned
if userUtils.isBanned(userID): #if userUtils.isBanned(userID):
userToken.enqueue(serverPackets.loginBanned()) # userToken.enqueue(serverPackets.loginBanned())
return # return
# Send restricted message if needed # Send restricted message if needed
if userToken.restricted: #if userToken.restricted:
userToken.checkRestricted(True) # userToken.checkRestricted(True)
# Change action packet # Change action packet
packetData = clientPackets.userActionChange(packetData) packetData = clientPackets.userActionChange(packetData)
@@ -34,8 +31,10 @@ if userToken.matchID != -1 and userToken.actionID != actions.MULTIPLAYING and us
''' '''
# Update cached stats if our pp changed if we've just submitted a score or we've changed gameMode # Update cached stats if our pp changed if we've just submitted a score or we've changed gameMode
if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]): #if (userToken.actionID == actions.PLAYING or userToken.actionID == actions.MULTIPLAYING) or (userToken.pp != userUtils.getPP(userID, userToken.gameMode)) or (userToken.gameMode != packetData["gameMode"]):
# Always update game mode, or we'll cache stats from the wrong game mode if we've changed it
# Update cached stats if we've changed gamemode
if userToken.gameMode != packetData["gameMode"]:
userToken.gameMode = packetData["gameMode"] userToken.gameMode = packetData["gameMode"]
userToken.updateCachedStats() userToken.updateCachedStats()

View File

@@ -1,7 +1,6 @@
from common.log import logUtils as log from common.log import logUtils as log
from constants import clientPackets from constants import clientPackets
from constants import exceptions from constants import exceptions
from constants import serverPackets
from objects import glob from objects import glob

View File

@@ -1,4 +1,3 @@
from common import generalUtils
from common.log import logUtils as log from common.log import logUtils as log
from constants import clientPackets from constants import clientPackets
from constants import exceptions from constants import exceptions

View File

@@ -15,6 +15,7 @@ from objects import glob
def handle(tornadoRequest): def handle(tornadoRequest):
# Data to return # Data to return
responseToken = None
responseTokenString = "ayy" responseTokenString = "ayy"
responseData = bytes() responseData = bytes()
@@ -29,9 +30,6 @@ def handle(tornadoRequest):
# 2:-3 thing is because requestData has some escape stuff that we don't need # 2:-3 thing is because requestData has some escape stuff that we don't need
loginData = str(tornadoRequest.request.body)[2:-3].split("\\n") loginData = str(tornadoRequest.request.body)[2:-3].split("\\n")
try: try:
# If true, print error to console
err = False
# Make sure loginData is valid # Make sure loginData is valid
if len(loginData) < 3: if len(loginData) < 3:
raise exceptions.invalidArgumentsException() raise exceptions.invalidArgumentsException()
@@ -220,25 +218,23 @@ def handle(tornadoRequest):
except exceptions.loginFailedException: except exceptions.loginFailedException:
# Login failed error packet # Login failed error packet
# (we don't use enqueue because we don't have a token since login has failed) # (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.loginFailed() responseData += serverPackets.loginFailed()
except exceptions.invalidArgumentsException: except exceptions.invalidArgumentsException:
# Invalid POST data # Invalid POST data
# (we don't use enqueue because we don't have a token since login has failed) # (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.loginFailed() responseData += serverPackets.loginFailed()
responseData += serverPackets.notification("I see what you're doing...") responseData += serverPackets.notification("I see what you're doing...")
except exceptions.loginBannedException: except exceptions.loginBannedException:
# Login banned error packet # Login banned error packet
err = True
responseData += serverPackets.loginBanned() responseData += serverPackets.loginBanned()
except exceptions.loginLockedException: except exceptions.loginLockedException:
# Login banned error packet # Login banned error packet
err = True
responseData += serverPackets.loginLocked() responseData += serverPackets.loginLocked()
except exceptions.banchoMaintenanceException: except exceptions.banchoMaintenanceException:
# Bancho is in maintenance mode # Bancho is in maintenance mode
responseData = responseToken.queue responseData = bytes()
if responseToken is not None:
responseData = responseToken.queue
responseData += serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.") responseData += serverPackets.notification("Our bancho server is in maintenance mode. Please try to login again later.")
responseData += serverPackets.loginFailed() responseData += serverPackets.loginFailed()
except exceptions.banchoRestartingException: except exceptions.banchoRestartingException:
@@ -251,7 +247,6 @@ def handle(tornadoRequest):
except exceptions.haxException: except exceptions.haxException:
# Using oldoldold client, we don't have client data. Force update. # Using oldoldold client, we don't have client data. Force update.
# (we don't use enqueue because we don't have a token since login has failed) # (we don't use enqueue because we don't have a token since login has failed)
err = True
responseData += serverPackets.forceUpdate() responseData += serverPackets.forceUpdate()
responseData += serverPackets.notification("Hory shitto, your client is TOO old! Nice prehistory! Please turn update it from the settings!") responseData += serverPackets.notification("Hory shitto, your client is TOO old! Nice prehistory! Please turn update it from the settings!")
except: except:
@@ -259,10 +254,7 @@ def handle(tornadoRequest):
finally: finally:
# Console and discord log # Console and discord log
if len(loginData) < 3: if len(loginData) < 3:
msg = "Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP) log.info("Invalid bancho login request from **{}** (insufficient POST data)".format(requestIP), "bunker")
else:
msg = "Bancho login request from **{}** for user **{}** ({})".format(requestIP, loginData[0], "failed" if err == True else "success")
log.info(msg, "bunker")
# Return token string and data # Return token string and data
return responseTokenString, responseData return responseTokenString, responseData

View File

@@ -1,4 +1,5 @@
import time import time
import json
from common.log import logUtils as log from common.log import logUtils as log
from constants import serverPackets from constants import serverPackets
@@ -6,7 +7,7 @@ from helpers import chatHelper as chat
from objects import glob from objects import glob
def handle(userToken, _=None): def handle(userToken, _=None, deleteToken=True):
# get usertoken data # get usertoken data
userID = userToken.userID userID = userToken.userID
username = userToken.username username = userToken.username
@@ -16,7 +17,7 @@ def handle(userToken, _=None):
# the old logout packet will still be in the queue and will be sent to # 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 # 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 the user logs out before 5 seconds, he will be disconnected later with timeout check
if (int(time.time()-userToken.loginTime) >= 5 or userToken.irc): if int(time.time() - userToken.loginTime) >= 5 or userToken.irc:
# Stop spectating # Stop spectating
userToken.stopSpectating() userToken.stopSpectating()
@@ -34,11 +35,23 @@ def handle(userToken, _=None):
glob.streams.broadcast("main", serverPackets.userLogout(userID)) glob.streams.broadcast("main", serverPackets.userLogout(userID))
# Disconnect from IRC if needed # Disconnect from IRC if needed
if userToken.irc == True and glob.irc == True: if userToken.irc and glob.irc:
glob.ircServer.forceDisconnection(userToken.username) glob.ircServer.forceDisconnection(userToken.username)
# Delete token # Delete token
glob.tokens.deleteToken(requestToken) if deleteToken:
glob.tokens.deleteToken(requestToken)
else:
userToken.kicked = True
# Change username if needed
newUsername = glob.redis.get("ripple:change_username_pending:{}".format(userID))
if newUsername is not None:
log.debug("Sending username change request for user {}".format(userID))
glob.redis.publish("peppy:change_username", json.dumps({
"userID": userID,
"newUsername": newUsername.decode("utf-8")
}))
# Console output # Console output
log.info("{} has been disconnected. (logout)".format(username)) log.info("{} has been disconnected. (logout)".format(username))

View File

@@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, packetData, has): def handle(userToken, _, has):
# Get usertoken data # Get usertoken data
userID = userToken.userID userID = userToken.userID

View File

@@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, packetData): def handle(userToken, _):
# Get usertoken data # Get usertoken data
userID = userToken.userID userID = userToken.userID

View File

@@ -1,5 +1,4 @@
from objects import glob from objects import glob
from constants import slotStatuses
from constants import serverPackets from constants import serverPackets
def handle(userToken, packetData): def handle(userToken, packetData):

View File

@@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, packetData): def handle(userToken, _):
# Get userToken data # Get userToken data
userID = userToken.userID userID = userToken.userID

View File

@@ -1,6 +1,6 @@
from objects import glob from objects import glob
def handle(userToken, packetData): def handle(userToken, _):
# Get userToken data # Get userToken data
userID = userToken.userID userID = userToken.userID

View File

@@ -1,6 +1,4 @@
from objects import glob from objects import glob
from constants import slotStatuses
from constants import serverPackets
def handle(userToken, _): def handle(userToken, _):

View File

@@ -1,17 +1,15 @@
from common.log import logUtils as log from common.log import logUtils as log
from helpers import chatHelper as chat from helpers import chatHelper as chat
from objects import glob
def handle(userToken, _): def handle(userToken, _):
# Get usertoken data # Get usertoken data
userID = userToken.userID
username = userToken.username username = userToken.username
# Remove user from users in lobby # Remove user from users in lobby
userToken.leaveStream("lobby") userToken.leaveStream("lobby")
# Part lobby channel # Part lobby channel
# Done automatically by the client
chat.partChannel(channel="#lobby", token=userToken, kick=True) chat.partChannel(channel="#lobby", token=userToken, kick=True)
# Console output # Console output

View File

@@ -1,6 +1,5 @@
from objects import glob from objects import glob
from constants import serverPackets from constants import serverPackets
from constants import exceptions
def handle(userToken, packetData): def handle(userToken, packetData):
# get token data # get token data

View File

@@ -1,5 +1,4 @@
from constants import clientPackets from constants import clientPackets
from constants import serverPackets
from objects import glob from objects import glob
def handle(userToken, packetData): def handle(userToken, packetData):

4
full_build.sh Normal file
View File

@@ -0,0 +1,4 @@
find . -name "*.c" -type f -delete
find . -name "*.o" -type f -delete
find . -name "*.so" -type f -delete
python3 setup.py build_ext --inplace

View File

@@ -1,6 +1,9 @@
import json import json
from common.log import logUtils as log import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
from helpers import chatHelper from helpers import chatHelper
@@ -8,6 +11,9 @@ from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}

View File

@@ -1,11 +1,19 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.ripple import userUtils
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}
@@ -18,7 +26,8 @@ class handler(requestsManager.asyncRequestHandler):
username = None username = None
userID = None userID = None
if "u" in self.request.arguments: if "u" in self.request.arguments:
username = self.get_argument("u").lower().replace(" ", "_") #username = self.get_argument("u").lower().replace(" ", "_")
username = userUtils.safeUsername(self.get_argument("u"))
else: else:
try: try:
userID = int(self.get_argument("id")) userID = int(self.get_argument("id"))

View File

@@ -1,16 +1,23 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}
try: try:
# Get online users count # Get online users count
data["result"] = len(glob.tokens.tokens) data["result"] = int(glob.redis.get("ripple:online_users").decode("utf-8"))
# Status code and message # Status code and message
statusCode = 200 statusCode = 200

View File

@@ -1,10 +1,17 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}

View File

@@ -1,11 +1,18 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
from objects import glob from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}

View File

@@ -1,5 +1,9 @@
import json import json
import tornado.web
import tornado.gen
from common.sentry import sentry
from common.log import logUtils as log from common.log import logUtils as log
from common.web import requestsManager from common.web import requestsManager
from constants import exceptions from constants import exceptions
@@ -8,6 +12,9 @@ from objects import glob
class handler(requestsManager.asyncRequestHandler): class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncGet(self): def asyncGet(self):
statusCode = 400 statusCode = 400
data = {"message": "unknown error"} data = {"message": "unknown error"}

View File

@@ -1,268 +0,0 @@
import datetime
import gzip
import sys
import traceback
import tornado.gen
import tornado.web
from raven.contrib.tornado import SentryMixin
from common.log import logUtils as log
from common.web import requestsManager
from constants import exceptions
from constants import packetIDs
from constants import serverPackets
from events import cantSpectateEvent
from events import changeActionEvent
from events import changeMatchModsEvent
from events import changeMatchPasswordEvent
from events import changeMatchSettingsEvent
from events import changeSlotEvent
from events import channelJoinEvent
from events import channelPartEvent
from events import createMatchEvent
from events import friendAddEvent
from events import friendRemoveEvent
from events import joinLobbyEvent
from events import joinMatchEvent
from events import loginEvent
from events import logoutEvent
from events import matchChangeTeamEvent
from events import matchCompleteEvent
from events import matchFailedEvent
from events import matchFramesEvent
from events import matchHasBeatmapEvent
from events import matchInviteEvent
from events import matchLockEvent
from events import matchNoBeatmapEvent
from events import matchPlayerLoadEvent
from events import matchReadyEvent
from events import matchSkipEvent
from events import matchStartEvent
from events import matchTransferHostEvent
from events import partLobbyEvent
from events import partMatchEvent
from events import requestStatusUpdateEvent
from events import sendPrivateMessageEvent
from events import sendPublicMessageEvent
from events import setAwayMessageEvent
from events import spectateFramesEvent
from events import startSpectatingEvent
from events import stopSpectatingEvent
from events import userPanelRequestEvent
from events import userStatsRequestEvent
from events import tournamentMatchInfoRequestEvent
from events import tournamentJoinMatchChannelEvent
from events import tournamentLeaveMatchChannelEvent
from helpers import packetHelper
from objects import glob
class handler(SentryMixin, requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
def asyncPost(self):
try:
# Track time if needed
if glob.outputRequestTime:
# Start time
st = datetime.datetime.now()
# Client's token string and request data
requestTokenString = self.request.headers.get("osu-token")
requestData = self.request.body
# Server's token string and request data
responseTokenString = "ayy"
responseData = bytes()
if requestTokenString is 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
# Make sure the token exists
if requestTokenString not in glob.tokens.tokens:
raise exceptions.tokenNotFoundException()
# Token exists, get its object and lock it
userToken = glob.tokens.tokens[requestTokenString]
userToken.lock.acquire()
# Keep reading packets until everything has been read
while pos < len(requestData):
# Get packet from stack starting from new packet
leftData = requestData[pos:]
# Get packet ID, data length and data
packetID = packetHelper.readPacketID(leftData)
dataLength = packetHelper.readPacketLength(leftData)
packetData = requestData[pos:(pos+dataLength+7)]
# 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)))
# Event handler
def handleEvent(ev):
def wrapper():
ev.handle(userToken, packetData)
return wrapper
eventHandler = {
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),
packetIDs.client_startSpectating: handleEvent(startSpectatingEvent),
packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent),
packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent),
packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent),
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),
packetIDs.client_tournamentMatchInfoRequest: handleEvent(tournamentMatchInfoRequestEvent),
packetIDs.client_tournamentJoinMatchChannel: handleEvent(tournamentJoinMatchChannelEvent),
packetIDs.client_tournamentLeaveMatchChannel: handleEvent(tournamentLeaveMatchChannelEvent),
}
# 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,
]
# 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))
# 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
# Token queue built, send it
responseTokenString = userToken.token
responseData = userToken.queue
userToken.resetQueue()
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 is not None:
# Update ping time for timeout
userToken.updatePingTime()
# Release token lock
userToken.lock.release()
if glob.outputRequestTime:
# End time
et = datetime.datetime.now()
# Total time:
tt = float((et.microsecond-st.microsecond)/1000)
log.debug("Request time: {}ms".format(tt))
# Send server's response to client
# We don't use token object because we might not have a token (failed login)
if glob.gzip:
# 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("Connection", "keep-alive")
self.add_header("Keep-Alive", "timeout=5, max=100")
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 = "<html><head><title>MA MAURO ESISTE?</title><style type='text/css'>body{width:30%}</style></head><body><pre>"
html += " _ __<br>"
html += " (_) / /<br>"
html += " ______ __ ____ ____ / /____<br>"
html += " / ___/ / _ \\/ _ \\/ / _ \\<br>"
html += " / / / / /_) / /_) / / ____/<br>"
html += "/__/ /__/ .___/ .___/__/ \\_____/<br>"
html += " / / / /<br>"
html += " /__/ /__/<br>"
html += "<b>PYTHON > ALL VERSION</b><br><br>"
html += "<marquee style='white-space:pre;'><br>"
html += " .. o .<br>"
html += " o.o o . o<br>"
html += " oo...<br>"
html += " __[]__<br>"
html += " phwr--> _\\:D/_/o_o_o_|__ <span style=\"font-family: 'Comic Sans MS'; font-size: 8pt;\">u wot m8</span><br>"
html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/<br>"
html += " \\ . .. .. . /<br>"
html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<br>"
html += "</marquee><br><strike>reverse engineering a protocol impossible to reverse engineer since always</strike><br>we are actually reverse engineering bancho successfully. for the third time.<br><br><i>&copy; Ripple team, 2016</i></pre></body></html>"
self.write(html)

266
handlers/mainHandler.pyx Normal file
View File

@@ -0,0 +1,266 @@
import datetime
import gzip
import sys
import traceback
import tornado.gen
import tornado.web
from raven.contrib.tornado import SentryMixin
from common.log import logUtils as log
from common.web import requestsManager
from constants import exceptions
from constants import packetIDs
from constants import serverPackets
from events import cantSpectateEvent
from events import changeActionEvent
from events import changeMatchModsEvent
from events import changeMatchPasswordEvent
from events import changeMatchSettingsEvent
from events import changeSlotEvent
from events import channelJoinEvent
from events import channelPartEvent
from events import createMatchEvent
from events import friendAddEvent
from events import friendRemoveEvent
from events import joinLobbyEvent
from events import joinMatchEvent
from events import loginEvent
from events import logoutEvent
from events import matchChangeTeamEvent
from events import matchCompleteEvent
from events import matchFailedEvent
from events import matchFramesEvent
from events import matchHasBeatmapEvent
from events import matchInviteEvent
from events import matchLockEvent
from events import matchNoBeatmapEvent
from events import matchPlayerLoadEvent
from events import matchReadyEvent
from events import matchSkipEvent
from events import matchStartEvent
from events import matchTransferHostEvent
from events import partLobbyEvent
from events import partMatchEvent
from events import requestStatusUpdateEvent
from events import sendPrivateMessageEvent
from events import sendPublicMessageEvent
from events import setAwayMessageEvent
from events import spectateFramesEvent
from events import startSpectatingEvent
from events import stopSpectatingEvent
from events import userPanelRequestEvent
from events import userStatsRequestEvent
from events import tournamentMatchInfoRequestEvent
from events import tournamentJoinMatchChannelEvent
from events import tournamentLeaveMatchChannelEvent
from helpers import packetHelper
from objects import glob
from common.sentry import sentry
class handler(requestsManager.asyncRequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
@sentry.captureTornado
def asyncPost(self):
# Track time if needed
if glob.outputRequestTime:
# Start time
st = datetime.datetime.now()
# Client's token string and request data
requestTokenString = self.request.headers.get("osu-token")
requestData = self.request.body
# Server's token string and request data
responseTokenString = "ayy"
responseData = bytes()
if requestTokenString is 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
# Make sure the token exists
if requestTokenString not in glob.tokens.tokens:
raise exceptions.tokenNotFoundException()
# Token exists, get its object and lock it
userToken = glob.tokens.tokens[requestTokenString]
userToken.lock.acquire()
# Keep reading packets until everything has been read
while pos < len(requestData):
# Get packet from stack starting from new packet
leftData = requestData[pos:]
# Get packet ID, data length and data
packetID = packetHelper.readPacketID(leftData)
dataLength = packetHelper.readPacketLength(leftData)
packetData = requestData[pos:(pos+dataLength+7)]
# 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)))
# Event handler
def handleEvent(ev):
def wrapper():
ev.handle(userToken, packetData)
return wrapper
eventHandler = {
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),
packetIDs.client_startSpectating: handleEvent(startSpectatingEvent),
packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent),
packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent),
packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent),
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),
packetIDs.client_tournamentMatchInfoRequest: handleEvent(tournamentMatchInfoRequestEvent),
packetIDs.client_tournamentJoinMatchChannel: handleEvent(tournamentJoinMatchChannelEvent),
packetIDs.client_tournamentLeaveMatchChannel: handleEvent(tournamentLeaveMatchChannelEvent),
}
# 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,
]
# 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))
# 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
# Token queue built, send it
responseTokenString = userToken.token
responseData = userToken.queue
userToken.resetQueue()
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 is not None:
# Update ping time for timeout
userToken.updatePingTime()
# Release token lock
userToken.lock.release()
# Delete token if kicked
if userToken.kicked:
glob.tokens.deleteToken(userToken)
if glob.outputRequestTime:
# End time
et = datetime.datetime.now()
# Total time:
tt = float((et.microsecond-st.microsecond)/1000)
log.debug("Request time: {}ms".format(tt))
# Send server's response to client
# We don't use token object because we might not have a token (failed login)
if glob.gzip:
# 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("Connection", "keep-alive")
self.add_header("Keep-Alive", "timeout=5, max=100")
self.add_header("Content-Type", "text/html; charset=UTF-8")
@tornado.web.asynchronous
@tornado.gen.engine
def asyncGet(self):
html = "<html><head><title>MA MAURO ESISTE?</title><style type='text/css'>body{width:30%}</style></head><body><pre>"
html += " _ __<br>"
html += " (_) / /<br>"
html += " ______ __ ____ ____ / /____<br>"
html += " / ___/ / _ \\/ _ \\/ / _ \\<br>"
html += " / / / / /_) / /_) / / ____/<br>"
html += "/__/ /__/ .___/ .___/__/ \\_____/<br>"
html += " / / / /<br>"
html += " /__/ /__/<br>"
html += "<b>PYTHON > ALL VERSION</b><br><br>"
html += "<marquee style='white-space:pre;'><br>"
html += " .. o .<br>"
html += " o.o o . o<br>"
html += " oo...<br>"
html += " __[]__<br>"
html += " phwr--> _\\:D/_/o_o_o_|__ <span style=\"font-family: 'Comic Sans MS'; font-size: 8pt;\">u wot m8</span><br>"
html += " \\\"\"\"\"\"\"\"\"\"\"\"\"\"\"/<br>"
html += " \\ . .. .. . /<br>"
html += "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<br>"
html += "</marquee><br><strike>reverse engineering a protocol impossible to reverse engineer since always</strike><br>we are actually reverse engineering bancho successfully. for the third time.<br><br><i>&copy; Ripple team, 2016</i></pre></body></html>"
self.write(html)

View File

@@ -27,44 +27,32 @@ def joinChannel(userID = 0, channel = "", token = None, toIRC = True):
raise exceptions.userNotFoundException raise exceptions.userNotFoundException
else: else:
token = token token = token
userID = token.userID
# Get usertoken data
username = token.username
# Normal channel, do check stuff # Normal channel, do check stuff
# Make sure the channel exists # Make sure the channel exists
if channel not in glob.channels.channels: if channel not in glob.channels.channels:
raise exceptions.channelUnknownException 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 # Add the channel to our joined channel
token.joinChannel(channel) token.joinChannel(glob.channels.channels[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) # Send channel joined (IRC)
if glob.irc == True and toIRC == True: if glob.irc == True and toIRC == True:
glob.ircServer.banchoJoinChannel(username, channel) glob.ircServer.banchoJoinChannel(token.username, channel)
# Console output # Console output
log.info("{} joined channel {}".format(username, channel)) log.info("{} joined channel {}".format(token.username, channel))
# IRC code return # IRC code return
return 0 return 0
except exceptions.channelNoPermissionsException: except exceptions.channelNoPermissionsException:
log.warning("{} attempted to join channel {}, but they have no read permissions".format(username, channel)) log.warning("{} attempted to join channel {}, but they have no read permissions".format(token.username, channel))
return 403 return 403
except exceptions.channelUnknownException: except exceptions.channelUnknownException:
log.warning("{} attempted to join an unknown channel ({})".format(username, channel)) log.warning("{} attempted to join an unknown channel ({})".format(token.username, channel))
return 403
except exceptions.userAlreadyInChannelException:
log.warning("User {} already in channel {}".format(token.username, channel))
return 403 return 403
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho") log.warning("User not connected to IRC/Bancho")
@@ -82,6 +70,10 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
""" """
try: try:
# Make sure the client is not drunk and sends partChannel when closing a PM tab
if not channel.startswith("#"):
return
# Get token if not defined # Get token if not defined
if token is None: if token is None:
token = glob.tokens.getTokenFromUserID(userID) token = glob.tokens.getTokenFromUserID(userID)
@@ -90,10 +82,6 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
raise exceptions.userNotFoundException raise exceptions.userNotFoundException
else: else:
token = token token = token
userID = token.userID
# Get usertoken data
username = token.username
# Determine internal/client name if needed # Determine internal/client name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels) # (toclient is used clientwise for #multiplayer and #spectator channels)
@@ -113,12 +101,20 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
# Make sure the channel exists # Make sure the channel exists
if channel not in glob.channels.channels: if channel not in glob.channels.channels:
raise exceptions.channelUnknownException raise exceptions.channelUnknownException()
# Make sure the user is in the channel
if channel not in token.joinedChannels:
raise exceptions.userNotInChannelException()
# Part channel (token-side and channel-side) # Part channel (token-side and channel-side)
channelObject = glob.channels.channels[channel] channelObject = glob.channels.channels[channel]
token.partChannel(channel) token.partChannel(channelObject)
channelObject.userPart(userID)
# Delete temporary channel if everyone left
if "chat/{}".format(channelObject.name) in glob.streams.streams:
if channelObject.temp == True and len(glob.streams.streams["chat/{}".format(channelObject.name)].clients) - 1 == 0:
glob.channels.removeChannel(channelObject.name)
# Force close tab if needed # Force close tab if needed
# NOTE: Maybe always needed, will check later # NOTE: Maybe always needed, will check later
@@ -127,16 +123,19 @@ def partChannel(userID = 0, channel = "", token = None, toIRC = True, kick = Fal
# IRC part # IRC part
if glob.irc == True and toIRC == True: if glob.irc == True and toIRC == True:
glob.ircServer.banchoPartChannel(username, channel) glob.ircServer.banchoPartChannel(token.username, channel)
# Console output # Console output
log.info("{} parted channel {} ({})".format(username, channel, channelClient)) log.info("{} parted channel {} ({})".format(token.username, channel, channelClient))
# Return IRC code # Return IRC code
return 0 return 0
except exceptions.channelUnknownException: except exceptions.channelUnknownException:
log.warning("{} attempted to part an unknown channel ({})".format(username, channel)) log.warning("{} attempted to part an unknown channel ({})".format(token.username, channel))
return 403 return 403
except exceptions.userNotInChannelException:
log.warning("{} attempted to part {}, but he's not in that channel".format(token.username, channel))
return 442
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho") log.warning("User not connected to IRC/Bancho")
return 442 # idk return 442 # idk
@@ -153,7 +152,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
:return: 0 if joined or other IRC code in case of error. Needed only on IRC-side :return: 0 if joined or other IRC code in case of error. Needed only on IRC-side
""" """
try: try:
tokenString = "" #tokenString = ""
# Get token object if not passed # Get token object if not passed
if token is None: if token is None:
token = glob.tokens.getTokenFromUsername(fro) token = glob.tokens.getTokenFromUsername(fro)
@@ -162,11 +161,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
else: else:
# token object alredy passed, get its string and its username (fro) # token object alredy passed, get its string and its username (fro)
fro = token.username fro = token.username
tokenString = token.token #tokenString = token.token
# Set some variables
userID = token.userID
username = token.username
# Make sure this is not a tournament client # Make sure this is not a tournament client
if token.tournament: if token.tournament:
@@ -180,12 +175,16 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
if token.isSilenced(): if token.isSilenced():
raise exceptions.userSilencedException() raise exceptions.userSilencedException()
# Redirect !report to FokaBot
if message.startswith("!report"):
to = "FokaBot"
# Determine internal name if needed # Determine internal name if needed
# (toclient is used clientwise for #multiplayer and #spectator channels) # (toclient is used clientwise for #multiplayer and #spectator channels)
toClient = to toClient = to
if to == "#spectator": if to == "#spectator":
if token.spectating is None: if token.spectating is None:
s = userID s = token.userID
else: else:
s = token.spectatingUserID s = token.spectatingUserID
to = "#spect_{}".format(s) to = "#spect_{}".format(s)
@@ -195,7 +194,6 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
toClient = "#spectator" toClient = "#spectator"
elif to.startswith("#multi_"): elif to.startswith("#multi_"):
toClient = "#multiplayer" toClient = "#multiplayer"
# Truncate message if > 2048 characters # Truncate message if > 2048 characters
message = message[:2048]+"..." if len(message) > 2048 else message message = message[:2048]+"..." if len(message) > 2048 else message
@@ -203,7 +201,7 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
message = glob.chatFilters.filterMessage(message) message = glob.chatFilters.filterMessage(message)
# Build packet bytes # Build packet bytes
packet = serverPackets.sendMessage(username, toClient, message) packet = serverPackets.sendMessage(token.username, toClient, message)
# Send the message # Send the message
isChannel = to.startswith("#") isChannel = to.startswith("#")
@@ -211,35 +209,31 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# CHANNEL # CHANNEL
# Make sure the channel exists # Make sure the channel exists
if to not in glob.channels.channels: if to not in glob.channels.channels:
raise exceptions.channelUnknownException raise exceptions.channelUnknownException()
# Make sure the channel is not in moderated mode # Make sure the channel is not in moderated mode
if glob.channels.channels[to].moderated == True and token.admin == False: if glob.channels.channels[to].moderated == True and token.admin == False:
raise exceptions.channelModeratedException raise exceptions.channelModeratedException()
# Make sure we have write permissions # Make sure we have write permissions
if glob.channels.channels[to].publicWrite == False and token.admin == False: if glob.channels.channels[to].publicWrite == False and token.admin == False:
raise exceptions.channelNoPermissionsException raise exceptions.channelNoPermissionsException()
# Add message in buffer
token.addMessageInBuffer(to, message)
# Everything seems fine, build recipients list and send packet # Everything seems fine, build recipients list and send packet
recipients = glob.channels.channels[to].connectedUsers[:] glob.streams.broadcast("chat/{}".format(to), packet, but=[token.token])
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: else:
# USER # USER
# Make sure recipient user is connected # Make sure recipient user is connected
recipientToken = glob.tokens.getTokenFromUsername(to) recipientToken = glob.tokens.getTokenFromUsername(to)
if recipientToken is None: if recipientToken is None:
raise exceptions.userNotFoundException raise exceptions.userNotFoundException()
# Make sure the recipient is not a tournament client # Make sure the recipient is not a tournament client
if recipientToken.tournament: #if recipientToken.tournament:
raise exceptions.userTournamentException() # raise exceptions.userTournamentException()
# Make sure the recipient is not restricted or we are FokaBot # Make sure the recipient is not restricted or we are FokaBot
if recipientToken.restricted == True and fro.lower() != "fokabot": if recipientToken.restricted == True and fro.lower() != "fokabot":
@@ -248,8 +242,8 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
# TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend # TODO: Make sure the recipient has not disabled PMs for non-friends or he's our friend
# Away check # Away check
if recipientToken.awayCheck(userID): if recipientToken.awayCheck(token.userID):
sendMessage(to, fro, "\x01ACTION is away: {message}\x01".format(code=chr(int(1)), message=recipientToken.awayMessage)) sendMessage(to, fro, "\x01ACTION is away: {}\x01".format(recipientToken.awayMessage))
# Check message templates (mods/admins only) # Check message templates (mods/admins only)
if message in messageTemplates.templates and token.admin == True: if message in messageTemplates.templates and token.admin == True:
@@ -263,38 +257,38 @@ def sendMessage(fro = "", to = "", message = "", token = None, toIRC = True):
glob.ircServer.banchoMessage(fro, to, message) glob.ircServer.banchoMessage(fro, to, message)
# Spam protection (ignore FokaBot) # Spam protection (ignore FokaBot)
if userID > 999: if token.userID > 999:
token.spamProtection() token.spamProtection()
# Fokabot message # Fokabot message
if isChannel == True or to.lower() == "fokabot": if isChannel == True or to.lower() == "fokabot":
fokaMessage = fokabot.fokabotResponse(username, to, message) fokaMessage = fokabot.fokabotResponse(token.username, to, message)
if fokaMessage: if fokaMessage:
sendMessage("FokaBot", to if isChannel else fro, fokaMessage) sendMessage("FokaBot", to if isChannel else fro, fokaMessage)
# File and discord logs (public chat only) # File and discord logs (public chat only)
if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")): if to.startswith("#") and not (message.startswith("\x01ACTION is playing") and to.startswith("#spect_")):
log.chat("{fro} @ {to}: {message}".format(fro=username, to=to, message=str(message.encode("utf-8")))) log.chat("{fro} @ {to}: {message}".format(fro=token.username, to=to, message=str(message.encode("utf-8"))))
glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=username, to=to, message=str(message.encode("utf-8"))[2:-1])) glob.schiavo.sendChatlog("**{fro} @ {to}:** {message}".format(fro=token.username, to=to, message=str(message.encode("utf-8"))[2:-1]))
return 0 return 0
except exceptions.userSilencedException: except exceptions.userSilencedException:
token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft())) token.enqueue(serverPackets.silenceEndTime(token.getSilenceSecondsLeft()))
log.warning("{} tried to send a message during silence".format(username)) log.warning("{} tried to send a message during silence".format(token.username))
return 404 return 404
except exceptions.channelModeratedException: except exceptions.channelModeratedException:
log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(username, to)) log.warning("{} tried to send a message to a channel that is in moderated mode ({})".format(token.username, to))
return 404 return 404
except exceptions.channelUnknownException: except exceptions.channelUnknownException:
log.warning("{} tried to send a message to an unknown channel ({})".format(username, to)) log.warning("{} tried to send a message to an unknown channel ({})".format(token.username, to))
return 403 return 403
except exceptions.channelNoPermissionsException: except exceptions.channelNoPermissionsException:
log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(username, to)) log.warning("{} tried to send a message to channel {}, but they have no write permissions".format(token.username, to))
return 404 return 404
except exceptions.userRestrictedException: except exceptions.userRestrictedException:
log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(username, to)) log.warning("{} tried to send a message {}, but the recipient is in restricted mode".format(token.username, to))
return 404 return 404
except exceptions.userTournamentException: except exceptions.userTournamentException:
log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(username, to)) log.warning("{} tried to send a message {}, but the recipient is a tournament client".format(token.username, to))
return 404 return 404
except exceptions.userNotFoundException: except exceptions.userNotFoundException:
log.warning("User not connected to IRC/Bancho") log.warning("User not connected to IRC/Bancho")

View File

@@ -74,7 +74,7 @@ class config:
self.config.get("localize","enable") self.config.get("localize","enable")
self.config.get("localize","ipapiurl") self.config.get("localize","ipapiurl")
return True return True
except: except configparser.Error:
return False return False
def generateDefaultConfig(self): def generateDefaultConfig(self):
@@ -127,9 +127,9 @@ class config:
self.config.set("discord", "devgroup", "") self.config.set("discord", "devgroup", "")
self.config.add_section("datadog") self.config.add_section("datadog")
self.config.set("datadog", "enable") self.config.set("datadog", "enable", "0")
self.config.set("datadog", "apikey") self.config.set("datadog", "apikey", "")
self.config.set("datadog", "appkey") self.config.set("datadog", "appkey", "")
self.config.add_section("irc") self.config.add_section("irc")
self.config.set("irc", "enable", "1") self.config.set("irc", "enable", "1")

View File

@@ -30,7 +30,7 @@ def getLocation(ip):
try: try:
# Try to get position from Pikolo Aul's Go-Sanic ip API # Try to get position from Pikolo Aul's Go-Sanic ip API
result = json.loads(urllib.request.urlopen("{}/{}".format(glob.conf.config["localize"]["ipapiurl"], 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])) return float(result[0]), float(result[1])
except: except:
log.error("Error in get position") log.error("Error in get position")
return (0, 0) return 0, 0

View File

@@ -1,15 +1,15 @@
import struct import struct
from constants import dataTypes from constants import dataTypes
def uleb128Encode(num): cpdef bytearray uleb128Encode(int num):
""" """
Encode an int to uleb128 Encode an int to uleb128
:param num: int to encode :param num: int to encode
:return: bytearray with encoded number :return: bytearray with encoded number
""" """
arr = bytearray() cdef bytearray arr = bytearray()
length = 0 cdef int length = 0
if num == 0: if num == 0:
return bytearray(b"\x00") return bytearray(b"\x00")
@@ -23,15 +23,16 @@ def uleb128Encode(num):
return arr return arr
def uleb128Decode(num): cpdef list uleb128Decode(bytes num):
""" """
Decode a uleb128 to int Decode a uleb128 to int
:param num: encoded uleb128 int :param num: encoded uleb128 int
:return: (total, length) :return: (total, length)
""" """
shift = 0 cdef int shift = 0
arr = [0,0] #total, length cdef list arr = [0,0] #total, length
cdef int b
while True: while True:
b = num[arr[1]] b = num[arr[1]]
@@ -43,7 +44,7 @@ def uleb128Decode(num):
return arr return arr
def unpackData(data, dataType): cpdef unpackData(bytes data, int dataType):
""" """
Unpacks a single section of a packet. Unpacks a single section of a packet.
@@ -74,7 +75,7 @@ def unpackData(data, dataType):
# Unpack # Unpack
return struct.unpack(unpackType, bytes(data))[0] return struct.unpack(unpackType, bytes(data))[0]
def packData(__data, dataType): cpdef bytes packData(__data, int dataType):
""" """
Packs a single section of a packet. Packs a single section of a packet.
@@ -82,8 +83,9 @@ def packData(__data, dataType):
:param dataType: data type :param dataType: data type
:return: packed bytes :return: packed bytes
""" """
data = bytes() # data to return cdef bytes data = bytes() # data to return
pack = True # if True, use pack. False only with strings cdef bint pack = True # if True, use pack. False only with strings
cdef str packType
# Get right pack Type # Get right pack Type
if dataType == dataTypes.BBYTES: if dataType == dataTypes.BBYTES:
@@ -134,7 +136,7 @@ def packData(__data, dataType):
return data return data
def buildPacket(__packet, __packetData=None): cpdef bytes buildPacket(int __packet, list __packetData = None):
""" """
Builds a packet Builds a packet
@@ -142,14 +144,16 @@ def buildPacket(__packet, __packetData=None):
:param __packetData: packet structure [[data, dataType], [data, dataType], ...] :param __packetData: packet structure [[data, dataType], [data, dataType], ...]
:return: packet bytes :return: packet bytes
""" """
# Set some variables # Default argument
if __packetData is None: if __packetData is None:
__packetData = [] __packetData = []
packetData = bytes() # Set some variables
packetLength = 0 cdef bytes packetData = bytes()
packetBytes = bytes() cdef int packetLength = 0
cdef bytes packetBytes = bytes()
# Pack packet data # Pack packet data
cdef list i
for i in __packetData: for i in __packetData:
packetData += packData(i[0], i[1]) packetData += packData(i[0], i[1])
@@ -163,7 +167,7 @@ def buildPacket(__packet, __packetData=None):
packetBytes += packetData # packet data packetBytes += packetData # packet data
return packetBytes return packetBytes
def readPacketID(stream): cpdef int readPacketID(bytes stream):
""" """
Read packetID (first two bytes) from a packet Read packetID (first two bytes) from a packet
@@ -172,7 +176,7 @@ def readPacketID(stream):
""" """
return unpackData(stream[0:2], dataTypes.UINT16) return unpackData(stream[0:2], dataTypes.UINT16)
def readPacketLength(stream): cpdef int readPacketLength(bytes stream):
""" """
Read packet data length (3:7 bytes) from a packet Read packet data length (3:7 bytes) from a packet
@@ -182,7 +186,7 @@ def readPacketLength(stream):
return unpackData(stream[3:7], dataTypes.UINT32) return unpackData(stream[3:7], dataTypes.UINT32)
def readPacketData(stream, structure=None, hasFirstBytes = True): cpdef readPacketData(bytes stream, list structure=None, bint hasFirstBytes = True):
""" """
Read packet data from `stream` according to `structure` Read packet data from `stream` according to `structure`
:param stream: packet bytes :param stream: packet bytes
@@ -191,12 +195,15 @@ def readPacketData(stream, structure=None, hasFirstBytes = True):
if False, `stream` has only packet data. Default: True if False, `stream` has only packet data. Default: True
:return: {name: unpackedValue, ...} :return: {name: unpackedValue, ...}
""" """
# Read packet ID (first 2 bytes) # Default list argument
if structure is None: if structure is None:
structure = [] structure = []
data = {}
# Read packet ID (first 2 bytes)
cdef dict data = {}
# Skip packet ID and packet length if needed # Skip packet ID and packet length if needed
cdef start, end
if hasFirstBytes: if hasFirstBytes:
end = 7 end = 7
start = 7 start = 7
@@ -205,6 +212,8 @@ def readPacketData(stream, structure=None, hasFirstBytes = True):
start = 0 start = 0
# Read packet # Read packet
cdef list i
cdef bint unpack
for i in structure: for i in structure:
start = end start = end
unpack = True unpack = True
@@ -239,7 +248,10 @@ def readPacketData(stream, structure=None, hasFirstBytes = True):
end = start+length[0]+length[1]+1 end = start+length[0]+length[1]+1
# Read bytes # Read bytes
data[i[0]] = ''.join(chr(j) for j in stream[start+1+length[1]:end]) #data[i[0]] = ''.join(chr(j) for j in stream[start+1+length[1]:end])
data[i[0]] = ""
for j in stream[start+1+length[1]:end]:
data[i[0]] += chr(j)
elif i[1] == dataTypes.BYTE: elif i[1] == dataTypes.BYTE:
end = start+1 end = start+1
elif i[1] == dataTypes.UINT16 or i[1] == dataTypes.SINT16: elif i[1] == dataTypes.UINT16 or i[1] == dataTypes.SINT16:

View File

@@ -43,7 +43,7 @@ def scheduleShutdown(sendRestartTime, restart, message = "", delay=20):
:return: :return:
""" """
# Console output # Console output
log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay)) log.info("Pep.py will {} in {} seconds!".format("restart" if restart else "shutdown", sendRestartTime+delay), "bunker")
log.info("Sending server restart packets in {} seconds...".format(sendRestartTime)) log.info("Sending server restart packets in {} seconds...".format(sendRestartTime))
# Send notification if set # Send notification if set

View File

@@ -136,7 +136,7 @@ class Client:
self.server.removeClient(self, quitmsg) self.server.removeClient(self, quitmsg)
# Bancho logout # Bancho logout
if callLogout: if callLogout and self.banchoUsername != "":
chat.IRCDisconnect(self.IRCUsername) chat.IRCDisconnect(self.IRCUsername)
@@ -350,11 +350,11 @@ class Client:
self.sendMotd() self.sendMotd()
self.__handleCommand = self.mainHandler self.__handleCommand = self.mainHandler
def quitHandler(self, command, arguments): def quitHandler(self, _, arguments):
"""QUIT command handler""" """QUIT command handler"""
self.disconnect(self.IRCUsername if len(arguments) < 1 else arguments[0]) self.disconnect(self.IRCUsername if len(arguments) < 1 else arguments[0])
def joinHandler(self, command, arguments): def joinHandler(self, _, arguments):
"""JOIN command handler""" """JOIN command handler"""
if len(arguments) < 1: if len(arguments) < 1:
self.reply461("JOIN") self.reply461("JOIN")
@@ -402,13 +402,15 @@ class Client:
self.replyCode(332, description, channel=channel) self.replyCode(332, description, channel=channel)
# Build connected users list # Build connected users list
users = glob.channels.channels[channel].connectedUsers[:] if "chat/{}".format(channel) not in glob.streams.streams:
self.reply403(channel)
continue
users = glob.streams.streams["chat/{}".format(channel)].clients
usernames = [] usernames = []
for user in users: for user in users:
token = glob.tokens.getTokenFromUserID(user) if user not in glob.tokens.tokens:
if token is None:
continue continue
usernames.append(chat.fixUsernameForIRC(token.username)) usernames.append(chat.fixUsernameForIRC(glob.tokens.tokens[user].username))
usernames = " ".join(usernames) usernames = " ".join(usernames)
# Send IRC users list # Send IRC users list
@@ -419,7 +421,7 @@ class Client:
self.reply403(channel) self.reply403(channel)
continue continue
def partHandler(self, command, arguments): def partHandler(self, _, arguments):
"""PART command handler""" """PART command handler"""
if len(arguments) < 1: if len(arguments) < 1:
self.reply461("PART") self.reply461("PART")
@@ -503,7 +505,7 @@ class Client:
"""LUSERS command handler""" """LUSERS command handler"""
self.sendLusers() self.sendLusers()
def pingHandler(self, command, arguments): def pingHandler(self, _, arguments):
"""PING command handler""" """PING command handler"""
if len(arguments) < 1: if len(arguments) < 1:
self.replyCode(409, "No origin specified") self.replyCode(409, "No origin specified")
@@ -514,7 +516,7 @@ class Client:
"""(fake) PONG command handler""" """(fake) PONG command handler"""
pass pass
def awayHandler(self, command, arguments): def awayHandler(self, _, arguments):
"""AWAY command handler""" """AWAY command handler"""
response = chat.IRCAway(self.banchoUsername, " ".join(arguments)) response = chat.IRCAway(self.banchoUsername, " ".join(arguments))
self.replyCode(response, "You are no longer marked as being away" if response == 305 else "You have been marked as being away") self.replyCode(response, "You are no longer marked as being away" if response == 305 else "You have been marked as being away")
@@ -619,12 +621,11 @@ class Server:
value.message(":{} PRIVMSG {} :{}".format(fro, to, message)) value.message(":{} PRIVMSG {} :{}".format(fro, to, message))
def removeClient(self, client, quitmsg): def removeClient(self, client, _):
""" """
Remove a client from connected clients Remove a client from connected clients
:param client: client object :param client: client object
:param quitmsg: QUIT argument, useless atm
:return: :return:
""" """
if client.socket in self.clients: if client.socket in self.clients:
@@ -637,6 +638,7 @@ class Server:
:return: :return:
""" """
# Sentry # Sentry
sentryClient = None
if glob.sentry: if glob.sentry:
sentryClient = raven.Client(glob.conf.config["sentry"]["ircdns"]) sentryClient = raven.Client(glob.conf.config["sentry"]["ircdns"])
@@ -669,7 +671,7 @@ class Server:
try: try:
self.clients[conn] = Client(self, conn) self.clients[conn] = Client(self, conn)
log.info("[IRC] Accepted connection from {}:{}".format(addr[0], addr[1])) log.info("[IRC] Accepted connection from {}:{}".format(addr[0], addr[1]))
except socket.error as e: except socket.error:
try: try:
conn.close() conn.close()
except: except:
@@ -688,7 +690,7 @@ class Server:
lastAliveCheck = now lastAliveCheck = now
except: except:
log.error("[IRC] Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc())) log.error("[IRC] Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
if glob.sentry: if glob.sentry and sentryClient is not None:
sentryClient.captureException() sentryClient.captureException()
def main(port=6667): def main(port=6667):

View File

@@ -1,5 +1,6 @@
# TODO: Rewrite this shit # TODO: Rewrite this shit
from common import generalUtils from common import generalUtils
from constants import serverPackets
from objects import glob from objects import glob
@@ -41,3 +42,20 @@ class banchoConfig:
""" """
self.config["banchoMaintenance"] = maintenance self.config["banchoMaintenance"] = maintenance
glob.db.execute("UPDATE bancho_settings SET value_int = %s WHERE name = 'bancho_maintenance'", [int(maintenance)]) glob.db.execute("UPDATE bancho_settings SET value_int = %s WHERE name = 'bancho_maintenance'", [int(maintenance)])
def reload(self):
# Reload settings from bancho_settings
glob.banchoConf.loadSettings()
# Reload channels too
glob.channels.loadChannels()
# And chat filters
glob.chatFilters.loadFilters()
# Send new channels and new bottom icon to everyone
glob.streams.broadcast("main", serverPackets.mainMenuIcon(glob.banchoConf.config["menuIcon"]))
glob.streams.broadcast("main", serverPackets.channelInfoEnd())
for key, value in glob.channels.channels.items():
if value.publicRead == True and value.hidden == False:
glob.streams.broadcast("main", serverPackets.channelInfo(key))

View File

@@ -18,7 +18,6 @@ class channel:
self.publicWrite = publicWrite self.publicWrite = publicWrite
self.moderated = False self.moderated = False
self.temp = temp self.temp = temp
self.connectedUsers = [999] # Fokabot is always connected to every channels (otherwise it doesn't show up in IRC users list)
self.hidden = hidden self.hidden = hidden
# Client name (#spectator/#multiplayer) # Client name (#spectator/#multiplayer)
@@ -28,27 +27,7 @@ class channel:
elif self.name.startswith("#multi_"): elif self.name.startswith("#multi_"):
self.clientName = "#multiplayer" self.clientName = "#multiplayer"
def userJoin(self, userID): # Make Foka join the channel
""" fokaToken = glob.tokens.getTokenFromUserID(999)
Add a user to connected users if fokaToken is not None:
fokaToken.joinChannel(self)
:param userID:
:return:
"""
if userID not in self.connectedUsers:
self.connectedUsers.append(userID)
def userPart(self, userID):
"""
Remove a user from connected users
:param userID:
:return:
"""
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)

View File

@@ -1,6 +1,7 @@
from common.log import logUtils as log from common.log import logUtils as log
from objects import channel from objects import channel
from objects import glob from objects import glob
from helpers import chatHelper as chat
class channelList: class channelList:
@@ -34,6 +35,7 @@ class channelList:
:param hidden: if True, thic channel won't be shown in channels list :param hidden: if True, thic channel won't be shown in channels list
:return: :return:
""" """
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, description, publicRead, publicWrite, temp, hidden) self.channels[name] = channel.channel(name, description, publicRead, publicWrite, temp, hidden)
log.info("Created channel {}".format(name)) log.info("Created channel {}".format(name))
@@ -47,6 +49,7 @@ class channelList:
""" """
if name in self.channels: if name in self.channels:
return False return False
glob.streams.add("chat/{}".format(name))
self.channels[name] = channel.channel(name, "Chat", True, True, True, True) self.channels[name] = channel.channel(name, "Chat", True, True, True, True)
log.info("Created temp channel {}".format(name)) log.info("Created temp channel {}".format(name))
@@ -60,5 +63,13 @@ class channelList:
if name not in self.channels: if name not in self.channels:
log.debug("{} is not in channels list".format(name)) log.debug("{} is not in channels list".format(name))
return return
#glob.streams.broadcast("chat/{}".format(name), serverPackets.channelKicked(name))
stream = glob.streams.getStream("chat/{}".format(name))
if stream is not None:
for token in stream.clients:
if token in glob.tokens.tokens:
chat.partChannel(channel=name, token=glob.tokens.tokens[token], kick=True)
glob.streams.dispose("chat/{}".format(name))
glob.streams.remove("chat/{}".format(name))
self.channels.pop(name) self.channels.pop(name)
log.info("Removed channel {}".format(name)) log.info("Removed channel {}".format(name))

View File

@@ -11,7 +11,7 @@ from common.web import schiavo
try: try:
with open("version") as f: with open("version") as f:
VERSION = f.read() VERSION = f.read().strip()
if VERSION == "": if VERSION == "":
raise Exception raise Exception
except: except:

View File

@@ -355,6 +355,7 @@ class match:
glob.streams.broadcast(self.streamName, serverPackets.matchComplete()) glob.streams.broadcast(self.streamName, serverPackets.matchComplete())
# Destroy playing stream # Destroy playing stream
glob.streams.dispose(self.playingStreamName)
glob.streams.remove(self.playingStreamName) glob.streams.remove(self.playingStreamName)
# Console output # Console output
@@ -375,7 +376,7 @@ class match:
""" """
Add someone to users in match Add someone to users in match
:param userID: user id of the user :param user: user object of the user
:return: True if join success, False if fail (room is full) :return: True if join success, False if fail (room is full)
""" """
# Make sure we're not in this match # Make sure we're not in this match
@@ -403,7 +404,7 @@ class match:
""" """
Remove someone from users in match Remove someone from users in match
:param userID: user if of the user :param user: user object of the user
:return: :return:
""" """
# Make sure the user is in room # Make sure the user is in room

View File

@@ -1,7 +1,6 @@
from objects import match from objects import match
from objects import glob from objects import glob
from constants import serverPackets from constants import serverPackets
from common.log import logUtils as log
class matchList: class matchList:
def __init__(self): def __init__(self):
@@ -40,9 +39,11 @@ class matchList:
return return
# Remove match object and stream # Remove match object and stream
match = self.matches.pop(matchID) _match = self.matches.pop(matchID)
glob.streams.remove(match.streamName) glob.streams.dispose(_match.streamName)
glob.streams.remove(match.playingStreamName) glob.streams.dispose(_match.playingStreamName)
glob.streams.remove(_match.streamName)
glob.streams.remove(_match.playingStreamName)
# Send match dispose packet to everyone in lobby # Send match dispose packet to everyone in lobby
glob.streams.broadcast("lobby", serverPackets.disposeMatch(matchID)) glob.streams.broadcast("lobby", serverPackets.disposeMatch(matchID))

View File

@@ -5,6 +5,7 @@ import uuid
from common.constants import gameModes, actions from common.constants import gameModes, actions
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils from common.ripple import userUtils
from constants import exceptions
from constants import serverPackets from constants import serverPackets
from events import logoutEvent from events import logoutEvent
from helpers import chatHelper as chat from helpers import chatHelper as chat
@@ -31,6 +32,7 @@ class token:
self.privileges = userUtils.getPrivileges(self.userID) self.privileges = userUtils.getPrivileges(self.userID)
self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer") or userUtils.isInPrivilegeGroup(self.userID, "community manager") self.admin = userUtils.isInPrivilegeGroup(self.userID, "developer") or userUtils.isInPrivilegeGroup(self.userID, "community manager")
self.irc = irc self.irc = irc
self.kicked = False
self.restricted = userUtils.isRestricted(self.userID) self.restricted = userUtils.isRestricted(self.userID)
self.loginTime = int(time.time()) self.loginTime = int(time.time())
self.pingTime = self.loginTime self.pingTime = self.loginTime
@@ -38,6 +40,7 @@ class token:
self.lock = threading.Lock() # Sync primitive self.lock = threading.Lock() # Sync primitive
self.streams = [] self.streams = []
self.tournament = tournament self.tournament = tournament
self.messagesBuffer = []
# Default variables # Default variables
self.spectators = [] self.spectators = []
@@ -95,9 +98,14 @@ class token:
""" """
Add bytes (packets) to queue Add bytes (packets) to queue
:param bytes: (packet) bytes to enqueue :param bytes_: (packet) bytes to enqueue
""" """
# TODO: reduce max queue size
# Never enqueue for IRC clients or Foka
if self.irc or self.userID < 999:
return
# Avoid memory leaks
if len(bytes_) < 10 * 10 ** 6: if len(bytes_) < 10 * 10 ** 6:
self.queue += bytes_ self.queue += bytes_
else: else:
@@ -107,29 +115,37 @@ class token:
"""Resets the queue. Call when enqueued packets have been sent""" """Resets the queue. Call when enqueued packets have been sent"""
self.queue = bytes() self.queue = bytes()
def joinChannel(self, channel): def joinChannel(self, channelObject):
""" """
Add channel to joined channels list Join a channel
:param channel: channel name :param channelObject: channel object
:raises: exceptions.userAlreadyInChannelException()
exceptions.channelNoPermissionsException()
""" """
if channel not in self.joinedChannels: if channelObject.name in self.joinedChannels:
self.joinedChannels.append(channel) raise exceptions.userAlreadyInChannelException()
if channelObject.publicRead == False and self.admin == False:
raise exceptions.channelNoPermissionsException()
self.joinedChannels.append(channelObject.name)
self.joinStream("chat/{}".format(channelObject.name))
self.enqueue(serverPackets.channelJoinSuccess(self.userID, channelObject.clientName))
def partChannel(self, channel): def partChannel(self, channelObject):
""" """
Remove channel from joined channels list Remove channel from joined channels list
:param channel: channel name :param channelObject: channel object
""" """
if channel in self.joinedChannels: self.joinedChannels.remove(channelObject.name)
self.joinedChannels.remove(channel) self.leaveStream("chat/{}".format(channelObject.name))
def setLocation(self, latitude, longitude): def setLocation(self, latitude, longitude):
""" """
Set client location Set client location
:param location: [latitude, longitude] :param latitude: latitude
:param longitude: longitude
""" """
self.location = (latitude, longitude) self.location = (latitude, longitude)
@@ -228,12 +244,12 @@ class token:
chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True) chat.partChannel(token=hostToken, channel="#spect_{}".format(hostToken.userID), kick=True)
hostToken.leaveStream(streamName) hostToken.leaveStream(streamName)
# Console output
log.info("{} is no longer spectating {}. Current spectators: {}".format(self.username, self.spectatingUserID, hostToken.spectators))
# Part #spectator channel # Part #spectator channel
chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True) chat.partChannel(token=self, channel="#spect_{}".format(self.spectatingUserID), kick=True)
# Console output
log.info("{} is no longer spectating {}".format(self.username, self.spectatingUserID))
# Set our spectating user to 0 # Set our spectating user to 0
self.spectating = None self.spectating = None
self.spectatingUserID = 0 self.spectatingUserID = 0
@@ -323,22 +339,28 @@ class token:
self.enqueue(serverPackets.loginFailed()) self.enqueue(serverPackets.loginFailed())
# Logout event # Logout event
logoutEvent.handle(self, None) logoutEvent.handle(self, deleteToken=self.irc)
def silence(self, seconds, reason, author = 999): def silence(self, seconds = None, reason = "", author = 999):
""" """
Silences this user (db, packet and token) Silences this user (db, packet and token)
:param seconds: silence length in seconds :param seconds: silence length in seconds. If None, get it from db. Default: None
:param reason: silence reason :param reason: silence reason. Default: empty string
:param author: userID of who has silenced the user. Default: 999 (FokaBot) :param author: userID of who has silenced the user. Default: 999 (FokaBot)
:return: :return:
""" """
# Silence in db and token if seconds is None:
self.silenceEndTime = int(time.time())+seconds # Get silence expire from db if needed
userUtils.silence(self.userID, seconds, reason, author) seconds = max(0, userUtils.getSilenceEnd(self.userID) - int(time.time()))
else:
# Silence in db and token
userUtils.silence(self.userID, seconds, reason, author)
# Send silence packet to target # Silence token
self.silenceEndTime = int(time.time()) + seconds
# Send silence packet to user
self.enqueue(serverPackets.silenceEndTime(seconds)) self.enqueue(serverPackets.silenceEndTime(seconds))
# Send silenced packet to everyone else # Send silenced packet to everyone else
@@ -394,18 +416,29 @@ class token:
self.gameRank = stats["gameRank"] self.gameRank = stats["gameRank"]
self.pp = stats["pp"] self.pp = stats["pp"]
def checkRestricted(self, force=False): def checkRestricted(self):
""" """
Check if this token is restricted. If so, send fokabot message Check if this token is restricted. If so, send fokabot message
:param force: If True, get restricted value from db.
If False, get the cached one. Default: False
:return: :return:
""" """
if force: oldRestricted = self.restricted
self.restricted = userUtils.isRestricted(self.userID) self.restricted = userUtils.isRestricted(self.userID)
if self.restricted: if self.restricted:
self.setRestricted() self.setRestricted()
elif not self.restricted and oldRestricted != self.restricted:
self.resetRestricted()
def checkBanned(self):
"""
Check if this user is banned. If so, disconnect it.
:return:
"""
if userUtils.isBanned(self.userID):
self.enqueue(serverPackets.loginBanned())
logoutEvent.handle(self, deleteToken=False)
def setRestricted(self): def setRestricted(self):
""" """
@@ -415,7 +448,16 @@ class token:
:return: :return:
""" """
self.restricted = True self.restricted = True
chat.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.")
def resetRestricted(self):
"""
Send FokaBot message to alert the user that he has been unrestricted
and he has to log in again.
:return:
"""
chat.sendMessage("FokaBot", self.username, "Your account has been unrestricted! Please log in again.")
def joinStream(self, name): def joinStream(self, name):
""" """
@@ -459,4 +501,25 @@ class token:
if self.awayMessage == "" or userID in self.sentAway: if self.awayMessage == "" or userID in self.sentAway:
return False return False
self.sentAway.append(userID) self.sentAway.append(userID)
return True return True
def addMessageInBuffer(self, chan, message):
"""
Add a message in messages buffer (10 messages, truncated at 50 chars).
Used as proof when the user gets reported.
:param chan: channel
:param message: message content
:return:
"""
if len(self.messagesBuffer) > 9:
self.messagesBuffer = self.messagesBuffer[1:]
self.messagesBuffer.append("{time} - {user}@{channel}: {message}".format(time=time.strftime("%H:%M", time.localtime()), user=self.username, channel=chan, message=message[:50]))
def getMessagesBufferString(self):
"""
Get the content of the messages buffer as a string
:return: messages buffer content as a string
"""
return "\n".join(x for x in self.messagesBuffer)

View File

@@ -43,15 +43,29 @@ class stream:
log.info("{} has left stream {}".format(token, self.name)) log.info("{} has left stream {}".format(token, self.name))
self.clients.remove(token) self.clients.remove(token)
def broadcast(self, data): def broadcast(self, data, but=None):
""" """
Send some data to all clients connected to this stream Send some data to all (or some) clients connected to this stream
:param data: data to send :param data: data to send
:param but: array of tokens to ignore. Default: None (send to everyone)
:return:
"""
if but is None:
but = []
for i in self.clients:
if i in glob.tokens.tokens:
if i not in but:
glob.tokens.tokens[i].enqueue(data)
else:
self.removeClient(token=i)
def dispose(self):
"""
Tell every client in this stream to leave the stream
:return: :return:
""" """
for i in self.clients: for i in self.clients:
if i in glob.tokens.tokens: if i in glob.tokens.tokens:
glob.tokens.tokens[i].enqueue(data) glob.tokens.tokens[i].leaveStream(self.name)
else:
self.removeClient(token=i)

View File

@@ -1,6 +1,7 @@
from objects import stream from objects import stream
from objects import glob from objects import glob
# TODO: use *args and **kwargs
class streamList: class streamList:
def __init__(self): def __init__(self):
self.streams = {} self.streams = {}
@@ -55,25 +56,39 @@ class streamList:
return return
self.streams[streamName].removeClient(client=client, token=token) self.streams[streamName].removeClient(client=client, token=token)
def broadcast(self, streamName, data): def broadcast(self, streamName, data, but=None):
""" """
Send some data to all clients in a stream Send some data to all clients in a stream
:param streamName: stream name :param streamName: stream name
:param data: data to send :param data: data to send
:param but: array of tokens to ignore. Default: None (send to everyone)
:return: :return:
""" """
if streamName not in self.streams: if streamName not in self.streams:
return return
self.streams[streamName].broadcast(data) self.streams[streamName].broadcast(data, but)
'''def getClients(self, streamName): def dispose(self, streamName, *args, **kwargs):
""" """
Get all clients in a stream Call `dispose` on `streamName`
:param streamName: name of the stream :param streamName: name of the stream
:param args:
:param kwargs:
:return: :return:
""" """
if streamName not in self.streams: if streamName not in self.streams:
return return
return self.streams[streamName].clients''' self.streams[streamName].dispose(*args, **kwargs)
def getStream(self, streamName):
"""
Returns streamName's stream object or None if it doesn't exist
:param streamName:
:return:
"""
if streamName in self.streams:
return self.streams[streamName]
return None

View File

@@ -1,6 +1,8 @@
import threading import threading
import time import time
import redis
from common.ripple import userUtils from common.ripple import userUtils
from common.log import logUtils as log from common.log import logUtils as log
from constants import serverPackets from constants import serverPackets
@@ -17,6 +19,7 @@ class tokenList:
Add a token object to tokens list Add a token object to tokens list
:param userID: user id associated to that token :param userID: user id associated to that token
:param ip: ip address of the client
:param irc: if True, set this token as IRC client :param irc: if True, set this token as IRC client
:param timeOffset: the time offset from UTC for this user. Default: 0. :param timeOffset: the time offset from UTC for this user. Default: 0.
:param tournament: if True, flag this client as a tournement client. Default: True. :param tournament: if True, flag this client as a tournement client. Default: True.
@@ -24,6 +27,7 @@ class tokenList:
""" """
newToken = osuToken.token(userID, ip=ip, irc=irc, timeOffset=timeOffset, tournament=tournament) newToken = osuToken.token(userID, ip=ip, irc=irc, timeOffset=timeOffset, tournament=tournament)
self.tokens[newToken.token] = newToken self.tokens[newToken.token] = newToken
glob.redis.incr("ripple:online_users")
return newToken return newToken
def deleteToken(self, token): def deleteToken(self, token):
@@ -34,12 +38,10 @@ class tokenList:
:return: :return:
""" """
if token in self.tokens: if token in self.tokens:
# Delete session from DB
if self.tokens[token].ip != "": if self.tokens[token].ip != "":
userUtils.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip) userUtils.deleteBanchoSessions(self.tokens[token].userID, self.tokens[token].ip)
# Pop token from list
self.tokens.pop(token) self.tokens.pop(token)
glob.redis.decr("ripple:online_users")
def getUserIDFromToken(self, token): def getUserIDFromToken(self, token):
""" """
@@ -55,25 +57,34 @@ class tokenList:
# Get userID associated to that token # Get userID associated to that token
return self.tokens[token].userID return self.tokens[token].userID
def getTokenFromUserID(self, userID, ignoreIRC=False): def getTokenFromUserID(self, userID, ignoreIRC=False, _all=False):
""" """
Get token from a user ID Get token from a user ID
:param userID: user ID to find :param userID: user ID to find
:param ignoreIRC: if True, consider bancho clients only and skip IRC clients :param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:param _all: if True, return a list with all clients that match given username, otherwise return
only the first occurrence.
:return: False if not found, token object if found :return: False if not found, token object if found
""" """
# Make sure the token exists # Make sure the token exists
ret = []
for _, value in self.tokens.items(): for _, value in self.tokens.items():
if value.userID == userID: if value.userID == userID:
if ignoreIRC and value.irc: if ignoreIRC and value.irc:
continue continue
return value if _all:
ret.append(value)
else:
return value
# Return none if not found # Return full list or None if not found
return None if _all:
return ret
else:
return None
def getTokenFromUsername(self, username, ignoreIRC=False, safe=False): def getTokenFromUsername(self, username, ignoreIRC=False, safe=False, _all=False):
""" """
Get an osuToken object from an username Get an osuToken object from an username
@@ -81,20 +92,29 @@ class tokenList:
:param ignoreIRC: if True, consider bancho clients only and skip IRC clients :param ignoreIRC: if True, consider bancho clients only and skip IRC clients
:param safe: if True, username is a safe username, :param safe: if True, username is a safe username,
compare it with token's safe username rather than normal username compare it with token's safe username rather than normal username
:param _all: if True, return a list with all clients that match given username, otherwise return
only the first occurrence.
:return: osuToken object or None :return: osuToken object or None
""" """
# lowercase # lowercase
who = username.lower() if not safe else username who = username.lower() if not safe else username
# Make sure the token exists # Make sure the token exists
ret = []
for _, value in self.tokens.items(): for _, value in self.tokens.items():
if (not safe and value.username.lower() == who) or (safe and value.safeUsername == who): if (not safe and value.username.lower() == who) or (safe and value.safeUsername == who):
if ignoreIRC and value.irc: if ignoreIRC and value.irc:
continue continue
return value if _all:
ret.append(value)
else:
return value
# Return none if not found # Return full list or None if not found
return None if _all:
return ret
else:
return None
def deleteOldTokens(self, userID): def deleteOldTokens(self, userID):
""" """
@@ -104,10 +124,15 @@ class tokenList:
:return: :return:
""" """
# Delete older tokens # Delete older tokens
delete = []
for key, value in list(self.tokens.items()): for key, value in list(self.tokens.items()):
if value.userID == userID: if value.userID == userID:
# Delete this token from the dictionary # Delete this token from the dictionary
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.", "kicked, multiple clients") #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.", "kicked, multiple clients")
delete.append(self.tokens[key])
for i in delete:
logoutEvent.handle(i)
def multipleEnqueue(self, packet, who, but = False): def multipleEnqueue(self, packet, who, but = False):
""" """
@@ -185,12 +210,16 @@ class tokenList:
def deleteBanchoSessions(self): def deleteBanchoSessions(self):
""" """
Truncate bancho_sessions table. Remove all `peppy:sessions:*` redis keys.
Call at bancho startup to delete old cached sessions Call at bancho startup to delete old cached sessions
:return: :return:
""" """
glob.db.execute("TRUNCATE TABLE bancho_sessions") try:
# TODO: Make function or some redis meme
glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:sessions:*")
except redis.RedisError:
pass
def tokenExists(self, username = "", userID = -1): def tokenExists(self, username = "", userID = -1):

65
pep.py
View File

@@ -1,9 +1,7 @@
"""Hello, pep.py here, ex-owner of ripple and prime minister of Ripwot."""
import os import os
import sys import sys
import threading import threading
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
import tornado.gen import tornado.gen
import tornado.httpserver import tornado.httpserver
import tornado.ioloop import tornado.ioloop
@@ -16,7 +14,7 @@ from common.constants import bcolors
from common.db import dbConnector from common.db import dbConnector
from common.ddog import datadogClient from common.ddog import datadogClient
from common.log import logUtils as log from common.log import logUtils as log
from common.ripple import userUtils from common.redis import pubSub
from common.web import schiavo from common.web import schiavo
from handlers import apiFokabotMessageHandler from handlers import apiFokabotMessageHandler
from handlers import apiIsOnlineHandler from handlers import apiIsOnlineHandler
@@ -34,7 +32,13 @@ from objects import banchoConfig
from objects import chatFilters from objects import chatFilters
from objects import fokabot from objects import fokabot
from objects import glob from objects import glob
from pubSubHandlers import changeUsernameHandler
from pubSubHandlers import disconnectHandler
from pubSubHandlers import banHandler
from pubSubHandlers import notificationHandler
from pubSubHandlers import updateSilenceHandler
from pubSubHandlers import updateStatsHandler
def make_app(): def make_app():
return tornado.web.Application([ return tornado.web.Application([
@@ -108,6 +112,8 @@ if __name__ == "__main__":
# Empty redis cache # Empty redis cache
try: try:
# TODO: Make function or some redis meme
glob.redis.set("ripple:online_users", 0)
glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:*") glob.redis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, "peppy:*")
except redis.exceptions.ResponseError: except redis.exceptions.ResponseError:
# Script returns error if there are no keys starting with peppy:* # Script returns error if there are no keys starting with peppy:*
@@ -136,7 +142,7 @@ if __name__ == "__main__":
consoleHelper.printNoNl("> Creating threads pool... ") consoleHelper.printNoNl("> Creating threads pool... ")
glob.pool = ThreadPool(int(glob.conf.config["server"]["threads"])) glob.pool = ThreadPool(int(glob.conf.config["server"]["threads"]))
consoleHelper.printDone() consoleHelper.printDone()
except: except ValueError:
consoleHelper.printError() consoleHelper.printError()
consoleHelper.printColored("[!] Error while creating threads pool. Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Error while creating threads pool. Please check your config.ini and run the server again", bcolors.RED)
@@ -149,6 +155,11 @@ if __name__ == "__main__":
consoleHelper.printColored("[!] Error while loading chat filters. Make sure there is a filters.txt file present", bcolors.RED) consoleHelper.printColored("[!] Error while loading chat filters. Make sure there is a filters.txt file present", bcolors.RED)
raise raise
# Start fokabot
consoleHelper.printNoNl("> Connecting FokaBot... ")
fokabot.connect()
consoleHelper.printDone()
# Initialize chat channels # Initialize chat channels
print("> Initializing chat channels... ") print("> Initializing chat channels... ")
glob.channels.loadChannels() glob.channels.loadChannels()
@@ -160,11 +171,6 @@ if __name__ == "__main__":
glob.streams.add("lobby") glob.streams.add("lobby")
consoleHelper.printDone() consoleHelper.printDone()
# Start fokabot
consoleHelper.printNoNl("> Connecting FokaBot... ")
fokabot.connect()
consoleHelper.printDone()
# Initialize user timeout check loop # Initialize user timeout check loop
consoleHelper.printNoNl("> Initializing user timeout check loop... ") consoleHelper.printNoNl("> Initializing user timeout check loop... ")
glob.tokens.usersTimeoutCheckLoop() glob.tokens.usersTimeoutCheckLoop()
@@ -182,7 +188,7 @@ if __name__ == "__main__":
# Discord # Discord
if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]): if generalUtils.stringToBool(glob.conf.config["discord"]["enable"]):
glob.schiavo = schiavo.schiavo(glob.conf.config["discord"]["boturl"]) glob.schiavo = schiavo.schiavo(glob.conf.config["discord"]["boturl"], "**pep.py**")
else: else:
consoleHelper.printColored("[!] Warning! Discord logging is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! Discord logging is disabled!", bcolors.YELLOW)
@@ -222,18 +228,16 @@ if __name__ == "__main__":
datadogClient.periodicCheck("online_users", lambda: len(glob.tokens.tokens)), datadogClient.periodicCheck("online_users", lambda: len(glob.tokens.tokens)),
datadogClient.periodicCheck("multiplayer_matches", lambda: len(glob.matches.matches)), datadogClient.periodicCheck("multiplayer_matches", lambda: len(glob.matches.matches)),
datadogClient.periodicCheck("ram_clients", lambda: generalUtils.getTotalSize(glob.tokens)), #datadogClient.periodicCheck("ram_clients", lambda: generalUtils.getTotalSize(glob.tokens)),
datadogClient.periodicCheck("ram_matches", lambda: generalUtils.getTotalSize(glob.matches)), #datadogClient.periodicCheck("ram_matches", lambda: generalUtils.getTotalSize(glob.matches)),
datadogClient.periodicCheck("ram_channels", lambda: generalUtils.getTotalSize(glob.channels)), #datadogClient.periodicCheck("ram_channels", lambda: generalUtils.getTotalSize(glob.channels)),
datadogClient.periodicCheck("ram_file_buffers", lambda: generalUtils.getTotalSize(glob.fileBuffers)), #datadogClient.periodicCheck("ram_file_buffers", lambda: generalUtils.getTotalSize(glob.fileBuffers)),
datadogClient.periodicCheck("ram_file_locks", lambda: generalUtils.getTotalSize(glob.fLocks)), #datadogClient.periodicCheck("ram_file_locks", lambda: generalUtils.getTotalSize(glob.fLocks)),
datadogClient.periodicCheck("ram_datadog", lambda: generalUtils.getTotalSize(glob.datadogClient)), #datadogClient.periodicCheck("ram_datadog", lambda: generalUtils.getTotalSize(glob.datadogClient)),
datadogClient.periodicCheck("ram_verified_cache", lambda: generalUtils.getTotalSize(glob.verifiedCache)), #datadogClient.periodicCheck("ram_verified_cache", lambda: generalUtils.getTotalSize(glob.verifiedCache)),
#datadogClient.periodicCheck("ram_userid_cache", lambda: generalUtils.getTotalSize(glob.userIDCache)), #datadogClient.periodicCheck("ram_irc", lambda: generalUtils.getTotalSize(glob.ircServer)),
#datadogClient.periodicCheck("ram_pool", lambda: generalUtils.getTotalSize(glob.pool)), #datadogClient.periodicCheck("ram_tornado", lambda: generalUtils.getTotalSize(glob.application)),
datadogClient.periodicCheck("ram_irc", lambda: generalUtils.getTotalSize(glob.ircServer)), #datadogClient.periodicCheck("ram_db", lambda: generalUtils.getTotalSize(glob.db)),
datadogClient.periodicCheck("ram_tornado", lambda: generalUtils.getTotalSize(glob.application)),
datadogClient.periodicCheck("ram_db", lambda: generalUtils.getTotalSize(glob.db)),
]) ])
else: else:
consoleHelper.printColored("[!] Warning! Datadog stats tracking is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! Datadog stats tracking is disabled!", bcolors.YELLOW)
@@ -244,9 +248,10 @@ if __name__ == "__main__":
glob.irc = generalUtils.stringToBool(glob.conf.config["irc"]["enable"]) glob.irc = generalUtils.stringToBool(glob.conf.config["irc"]["enable"])
if glob.irc: if glob.irc:
# IRC port # IRC port
ircPort = 0
try: try:
ircPort = int(glob.conf.config["irc"]["port"]) ircPort = int(glob.conf.config["irc"]["port"])
except: except ValueError:
consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Invalid IRC port! Please check your config.ini and run the server again", bcolors.RED)
log.logMessage("**pep.py** IRC server started!", discord="bunker", of="info.txt", stdout=False) log.logMessage("**pep.py** IRC server started!", discord="bunker", of="info.txt", stdout=False)
consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN) consoleHelper.printColored("> IRC server listening on 127.0.0.1:{}...".format(ircPort), bcolors.GREEN)
@@ -255,15 +260,27 @@ if __name__ == "__main__":
consoleHelper.printColored("[!] Warning! IRC server is disabled!", bcolors.YELLOW) consoleHelper.printColored("[!] Warning! IRC server is disabled!", bcolors.YELLOW)
# Server port # Server port
serverPort = 0
try: try:
serverPort = int(glob.conf.config["server"]["port"]) serverPort = int(glob.conf.config["server"]["port"])
except: except ValueError:
consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED) consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED)
# Server start message and console output # Server start message and console output
log.logMessage("**pep.py** Server started!", discord="bunker", of="info.txt", stdout=False) log.logMessage("**pep.py** Server started!", discord="bunker", of="info.txt", stdout=False)
consoleHelper.printColored("> Tornado listening for HTTP(s) 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)
# Connect to pubsub channels
pubSub.listener(glob.redis, {
"peppy:disconnect": disconnectHandler.handler(),
"peppy:change_username": changeUsernameHandler.handler(),
"peppy:reload_settings": lambda x: x == b"reload" and glob.banchoConf.reload(),
"peppy:update_cached_stats": updateStatsHandler.handler(),
"peppy:silence": updateSilenceHandler.handler(),
"peppy:ban": banHandler.handler(),
"peppy:notification": notificationHandler.handler(),
}).start()
# Start tornado # Start tornado
glob.application.listen(serverPort) glob.application.listen(serverPort)
tornado.ioloop.IOLoop.instance().start() tornado.ioloop.IOLoop.instance().start()

View File

View File

@@ -0,0 +1,18 @@
from common.redis import generalPubSubHandler
from common.ripple import userUtils
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.privileges = userUtils.getPrivileges(userID)
targetToken.checkBanned()
targetToken.checkRestricted()

View File

@@ -0,0 +1,50 @@
from common.redis import generalPubSubHandler
from common.ripple import userUtils
from common.log import logUtils as log
from common.constants import actions
from objects import glob
def handleUsernameChange(userID, newUsername, targetToken=None):
try:
userUtils.appendNotes(userID, "-- Username change: '{}' -> '{}'".format(userUtils.getUsername(userID), newUsername))
userUtils.changeUsername(userID, newUsername=newUsername)
if targetToken is not None:
targetToken.kick("Your username has been changed to {}. Please log in again.".format(newUsername), "username_change")
except userUtils.usernameAlreadyInUseError:
log.rap(999, "Username change: {} is already in use!", through="Bancho")
if targetToken is not None:
targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail")
except userUtils.invalidUsernameError:
log.rap(999, "Username change: {} is not a valid username!", through="Bancho")
if targetToken is not None:
targetToken.kick("There was a critical error while trying to change your username. Please contact a developer.", "username_change_fail")
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"newUsername": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
# Get the user's token
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is None:
# If the user is offline change username immediately
handleUsernameChange(data["userID"], data["newUsername"])
else:
if targetToken.irc or (targetToken.actionID != actions.PLAYING and targetToken.actionID != actions.MULTIPLAYING):
# If the user is online and he's connected through IRC or he's not playing,
# change username and kick the user immediately
handleUsernameChange(data["userID"], data["newUsername"], targetToken)
else:
# If the user is playing, delay the username change until he submits the score
# On submit modular, lets will send the username change request again
# through redis once the score has been submitted
# The check is performed on bancho logout too, so if the user disconnects
# without submitting a score, the username gets changed on bancho logout
glob.redis.set("ripple:change_username_pending:{}".format(data["userID"]), data["newUsername"])

View File

@@ -0,0 +1,18 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"reason": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is not None:
targetToken.kick(data["reason"], "pubsub_kick")

View File

@@ -0,0 +1,19 @@
from common.redis import generalPubSubHandler
from objects import glob
from constants import serverPackets
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.structure = {
"userID": 0,
"message": ""
}
def handle(self, data):
data = super().parseData(data)
if data is None:
return
targetToken = glob.tokens.getTokenFromUserID(data["userID"])
if targetToken is not None:
targetToken.enqueue(serverPackets.notification(data["message"]))

View File

@@ -0,0 +1,15 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.silence()

View File

@@ -0,0 +1,15 @@
from common.redis import generalPubSubHandler
from objects import glob
class handler(generalPubSubHandler.generalPubSubHandler):
def __init__(self):
super().__init__()
self.type = "int"
def handle(self, userID):
userID = super().parseData(userID)
if userID is None:
return
targetToken = glob.tokens.getTokenFromUserID(userID)
if targetToken is not None:
targetToken.updateCachedStats()

View File

@@ -5,4 +5,6 @@ psutil
raven raven
bcrypt>=3.1.1 bcrypt>=3.1.1
dill dill
redis redis
cython
datadog

17
setup.py Normal file
View File

@@ -0,0 +1,17 @@
"""Cython build file"""
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import os
cythonExt = []
for root, dirs, files in os.walk(os.getcwd()):
for file in files:
if file.endswith(".pyx"):
filePath = os.path.relpath(os.path.join(root, file))
cythonExt.append(Extension(filePath.replace("/", ".")[:-4], [filePath]))
setup(
name = "pep.pyx modules",
ext_modules = cythonize(cythonExt, nthreads = 4),
)

View File

@@ -1 +1 @@
1.10.0 1.11.2