.BANCHO. Merge branch tornado-gevent into master
This commit is contained in:
commit
de47c46d25
|
@ -10,14 +10,14 @@ This is Ripple's bancho server. It handles:
|
|||
## Requirements
|
||||
- Python 3.5
|
||||
- MySQLdb (`mysqlclient` or `mysql-python`)
|
||||
- Bottle
|
||||
- Tornado
|
||||
- Gevent
|
||||
- Bcrypt
|
||||
|
||||
## How to set up pep.py
|
||||
First of all, install all the dependencies
|
||||
```
|
||||
$ pip install mysqlclient bottle gevent bcrypt
|
||||
$ pip install mysqlclient tornado gevent bcrypt
|
||||
```
|
||||
then, run pep.py once to create the default config file and edit it
|
||||
```
|
||||
|
|
|
@ -586,7 +586,6 @@ def tillerinoLast(fro, chan, message):
|
|||
stars = oppaiData["stars"]
|
||||
|
||||
msg += " | {0:.2f} stars".format(stars)
|
||||
|
||||
return msg
|
||||
except Exception as a:
|
||||
log.error(a)
|
||||
|
|
|
@ -2,22 +2,27 @@ from helpers import userHelper
|
|||
from constants import serverPackets
|
||||
from constants import exceptions
|
||||
from objects import glob
|
||||
from helpers import consoleHelper
|
||||
from constants import bcolors
|
||||
from helpers import locationHelper
|
||||
from helpers import countryHelper
|
||||
import time
|
||||
from helpers import generalFunctions
|
||||
import sys
|
||||
import traceback
|
||||
from helpers import requestHelper
|
||||
from helpers import discordBotHelper
|
||||
from helpers import logHelper as log
|
||||
from helpers import chatHelper as chat
|
||||
from constants import privileges
|
||||
from helpers import requestHelper
|
||||
|
||||
def handle(bottleRequest):
|
||||
def handle(tornadoRequest):
|
||||
# Data to return
|
||||
responseTokenString = "ayy"
|
||||
responseData = bytes()
|
||||
|
||||
# Get IP from tornado request
|
||||
requestIP = requestHelper.getRequestIP(bottleRequest)
|
||||
requestIP = tornadoRequest.getRequestIP()
|
||||
|
||||
# Avoid exceptions
|
||||
clientData = ["unknown", "unknown", "unknown", "unknown", "unknown"]
|
||||
|
@ -25,8 +30,7 @@ def handle(bottleRequest):
|
|||
|
||||
# Split POST body so we can get username/password/hardware data
|
||||
# 2:-3 thing is because requestData has some escape stuff that we don't need
|
||||
postBody = bottleRequest.body.read()
|
||||
loginData = str(postBody)[2:-3].split("\\n")
|
||||
loginData = str(tornadoRequest.request.body)[2:-3].split("\\n")
|
||||
try:
|
||||
# If true, print error to console
|
||||
err = False
|
||||
|
@ -244,4 +248,4 @@ def handle(bottleRequest):
|
|||
log.info(msg, "bunker")
|
||||
|
||||
# Return token string and data
|
||||
return (responseTokenString, responseData)
|
||||
return (responseTokenString, responseData)
|
|
@ -1,37 +1,39 @@
|
|||
from helpers import requestHelper
|
||||
from constants import exceptions
|
||||
import json
|
||||
from objects import glob
|
||||
from helpers import chatHelper
|
||||
import bottle
|
||||
from helpers import logHelper as log
|
||||
|
||||
@bottle.route("/api/v1/fokabotMessage")
|
||||
def GETApiFokabotMessage():
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if "k" not in bottle.request.query or "to" not in bottle.request.query or "msg" not in bottle.request.query:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Check ci key
|
||||
key = bottle.request.query["k"]
|
||||
if key is None or key != glob.conf.config["server"]["cikey"]:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Send chat message
|
||||
chatHelper.sendMessage("FokaBot", bottle.request.query["to"], bottle.request.query["msg"])
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
class handler(requestHelper.asyncRequestHandler):
|
||||
def asyncGet(self):
|
||||
statusCode = 400
|
||||
data["message"] = "invalid parameters"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if requestHelper.checkArguments(self.request.arguments, ["k", "to", "msg"]) == False:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Send response
|
||||
bottle.response.status = statusCode
|
||||
bottle.response.add_header("Content-Type", "application/json")
|
||||
yield json.dumps(data)
|
||||
# Check ci key
|
||||
key = self.get_argument("k")
|
||||
if key is None or key != glob.conf.config["server"]["cikey"]:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
log.info("API REQUEST FOR FOKABOT MESSAGE AAAAAAA")
|
||||
chatHelper.sendMessage("FokaBot", self.get_argument("to"), self.get_argument("msg"))
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
statusCode = 400
|
||||
data["message"] = "invalid parameters"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
# Send response
|
||||
#self.clear()
|
||||
self.write(json.dumps(data))
|
||||
self.set_status(statusCode)
|
||||
#self.finish(json.dumps(data))
|
||||
|
|
|
@ -1,39 +1,48 @@
|
|||
from helpers import requestHelper
|
||||
from constants import exceptions
|
||||
import json
|
||||
from objects import glob
|
||||
import bottle
|
||||
|
||||
@bottle.route("/api/v1/isOnline")
|
||||
def GETApiIsOnline():
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if "u" not in bottle.request.query and "id" not in bottle.request.query:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Get online staus
|
||||
if "u" in bottle.request.query:
|
||||
username = bottle.request.query["u"]
|
||||
data["result"] = True if glob.tokens.getTokenFromUsername(username) != None else False
|
||||
else:
|
||||
try:
|
||||
userID = int(bottle.request.query["id"])
|
||||
data["result"] = True if glob.tokens.getTokenFromUserID(userID) != None else False
|
||||
except:
|
||||
class handler(requestHelper.asyncRequestHandler):
|
||||
def asyncGet(self):
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if "u" not in self.request.arguments and "id" not in self.request.arguments:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
statusCode = 400
|
||||
data["message"] = "missing required arguments"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
# Get online staus
|
||||
username = None
|
||||
userID = None
|
||||
if "u" in self.request.arguments:
|
||||
username = self.get_argument("u")
|
||||
else:
|
||||
try:
|
||||
userID = int(self.get_argument("id"))
|
||||
except:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Send response
|
||||
bottle.response.status = statusCode
|
||||
bottle.response.add_header("Content-Type", "application/json")
|
||||
yield json.dumps(data)
|
||||
if username == None and userID == None:
|
||||
data["result"] = False
|
||||
else:
|
||||
if username != None:
|
||||
data["result"] = True if glob.tokens.getTokenFromUsername(username) != None else False
|
||||
else:
|
||||
data["result"] = True if glob.tokens.getTokenFromUserID(userID) != None else False
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
statusCode = 400
|
||||
data["message"] = "missing required arguments"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
# Send response
|
||||
#self.clear()
|
||||
self.write(json.dumps(data))
|
||||
self.set_status(statusCode)
|
||||
#self.finish(json.dumps(data))
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
from helpers import requestHelper
|
||||
import json
|
||||
from objects import glob
|
||||
import bottle
|
||||
|
||||
@bottle.route("/api/v1/onlineUsers")
|
||||
def GETApiOnlineUsers():
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Get online users count
|
||||
data["result"] = len(glob.tokens.tokens)
|
||||
class handler(requestHelper.asyncRequestHandler):
|
||||
def asyncGet(self):
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Get online users count
|
||||
data["result"] = len(glob.tokens.tokens)
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
# Send response
|
||||
bottle.response.status = statusCode
|
||||
bottle.response.add_header("Content-Type", "application/json")
|
||||
yield json.dumps(data)
|
||||
# Send response
|
||||
#self.clear()
|
||||
self.write(json.dumps(data))
|
||||
self.set_status(statusCode)
|
||||
#self.finish(json.dumps(data))
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
from helpers import requestHelper
|
||||
import json
|
||||
from objects import glob
|
||||
import bottle
|
||||
|
||||
@bottle.route("/api/v1/serverStatus")
|
||||
def GETApiServerStatus():
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Get online users count
|
||||
data["result"] = -1 if glob.restarting == True else 1
|
||||
class handler(requestHelper.asyncRequestHandler):
|
||||
def asyncGet(self):
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Get online users count
|
||||
data["result"] = -1 if glob.restarting == True else 1
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
# Send response
|
||||
bottle.response.status = statusCode
|
||||
bottle.response.add_header("Content-Type", "application/json")
|
||||
yield json.dumps(data)
|
||||
# Send response
|
||||
#self.clear()
|
||||
self.write(json.dumps(data))
|
||||
self.set_status(statusCode)
|
||||
#self.finish(json.dumps(data))
|
||||
|
|
|
@ -1,48 +1,46 @@
|
|||
from helpers import requestHelper
|
||||
from helpers import logHelper as log
|
||||
import json
|
||||
from objects import glob
|
||||
from constants import exceptions
|
||||
import bottle
|
||||
|
||||
@bottle.route("/api/v1/verifiedStatus")
|
||||
def GETApiVerifiedStatus():
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if "u" not in bottle.request.query:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Get userID and its verified cache thing
|
||||
# -1: Not in cache
|
||||
# 0: Not verified (multiacc)
|
||||
# 1: Verified
|
||||
userID = bottle.request.query["u"]
|
||||
callback = None
|
||||
if "callback" in bottle.request.query:
|
||||
callback = bottle.request.query["callback"]
|
||||
data["result"] = -1 if userID not in glob.verifiedCache else glob.verifiedCache[userID]
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
class handler(requestHelper.asyncRequestHandler):
|
||||
def asyncGet(self):
|
||||
statusCode = 400
|
||||
data["message"] = "missing required arguments"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if requestHelper.checkArguments(self.request.arguments, ["u"]) == False:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Send response
|
||||
bottle.response.add_header("Access-Control-Allow-Origin", "*")
|
||||
bottle.response.add_header("Content-Type", "application/json")
|
||||
# Get userID and its verified cache thing
|
||||
# -1: Not in cache
|
||||
# 0: Not verified (multiacc)
|
||||
# 1: Verified
|
||||
userID = self.get_argument("u")
|
||||
data["result"] = -1 if userID not in glob.verifiedCache else glob.verifiedCache[userID]
|
||||
|
||||
# jquery meme
|
||||
output = ""
|
||||
if callback != None:
|
||||
output += callback+"("
|
||||
output += json.dumps(data)
|
||||
if callback != None:
|
||||
output += ")"
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
statusCode = 400
|
||||
data["message"] = "missing required arguments"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
bottle.response.status = statusCode
|
||||
yield output
|
||||
# Send response
|
||||
self.add_header("Access-Control-Allow-Origin", "*")
|
||||
self.add_header("Content-Type", "application/json")
|
||||
|
||||
# jquery meme
|
||||
output = ""
|
||||
if "callback" in self.request.arguments:
|
||||
output += self.get_argument("callback")+"("
|
||||
output += json.dumps(data)
|
||||
if "callback" in self.request.arguments:
|
||||
output += ")"
|
||||
|
||||
self.write(output)
|
||||
self.set_status(statusCode)
|
||||
|
|
|
@ -1,38 +1,39 @@
|
|||
from helpers import requestHelper
|
||||
from constants import exceptions
|
||||
import json
|
||||
from objects import glob
|
||||
from helpers import systemHelper
|
||||
from helpers import logHelper as log
|
||||
import bottle
|
||||
|
||||
@bottle.route("/api/v1/ciTrigger")
|
||||
def GETCiTrigger():
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if "k" not in bottle.request.query:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
class handler(requestHelper.asyncRequestHandler):
|
||||
def asyncGet(self):
|
||||
statusCode = 400
|
||||
data = {"message": "unknown error"}
|
||||
try:
|
||||
# Check arguments
|
||||
if requestHelper.checkArguments(self.request.arguments, ["k"]) == False:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
# Check ci key
|
||||
key = bottle.request.query["k"]
|
||||
if key != glob.conf.config["server"]["cikey"]:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
# Check ci key
|
||||
key = self.get_argument("k")
|
||||
if key is None or key != glob.conf.config["server"]["cikey"]:
|
||||
raise exceptions.invalidArgumentsException()
|
||||
|
||||
log.info("Ci event triggered!!")
|
||||
systemHelper.scheduleShutdown(5, False, "A new Bancho update is available and the server will be restarted in 5 seconds. Thank you for your patience.")
|
||||
log.info("Ci event triggered!!")
|
||||
systemHelper.scheduleShutdown(5, False, "A new Bancho update is available and the server will be restarted in 5 seconds. Thank you for your patience.")
|
||||
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
statusCode = 403
|
||||
data["message"] = "invalid ci key"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
# Status code and message
|
||||
statusCode = 200
|
||||
data["message"] = "ok"
|
||||
except exceptions.invalidArgumentsException:
|
||||
statusCode = 400
|
||||
data["message"] = "invalid ci key"
|
||||
finally:
|
||||
# Add status code to data
|
||||
data["status"] = statusCode
|
||||
|
||||
# Send response
|
||||
bottle.response.status = statusCode
|
||||
bottle.response.add_header("Content-Type", "application/json")
|
||||
yield json.dumps(data)
|
||||
# Send response
|
||||
#self.clear()
|
||||
self.write(json.dumps(data))
|
||||
self.set_status(statusCode)
|
||||
#self.finish(json.dumps(data))
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import bottle
|
||||
import datetime
|
||||
import gzip
|
||||
from helpers import requestHelper
|
||||
from objects import glob
|
||||
from helpers import packetHelper
|
||||
from helpers import logHelper as log
|
||||
from constants import exceptions
|
||||
from constants import packetIDs
|
||||
from helpers import packetHelper
|
||||
from constants import serverPackets
|
||||
from events import sendPublicMessageEvent
|
||||
from events import sendPrivateMessageEvent
|
||||
|
@ -47,199 +46,219 @@ from events import userStatsRequestEvent
|
|||
from events import requestStatusUpdateEvent
|
||||
from events import userPanelRequestEvent
|
||||
|
||||
@bottle.route("/", method="POST")
|
||||
def POSTMain():
|
||||
# Track time if needed
|
||||
if glob.outputRequestTime == True:
|
||||
# Start time
|
||||
st = datetime.datetime.now()
|
||||
# Exception tracking
|
||||
import tornado.web
|
||||
import tornado.gen
|
||||
import sys
|
||||
import traceback
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
from helpers import logHelper as log
|
||||
|
||||
# Client's token string and request data
|
||||
#requestTokenString = bottle.request.headers.get("osu-token")
|
||||
requestTokenString = bottle.request.get_header("osu-token")
|
||||
requestData = bottle.request.body.read()
|
||||
|
||||
# Server's token string and request data
|
||||
responseTokenString = "ayy"
|
||||
responseData = bytes()
|
||||
|
||||
if requestTokenString == None:
|
||||
# No token, first request. Handle login.
|
||||
responseTokenString, responseData = loginEvent.handle(bottle.request)
|
||||
else:
|
||||
userToken = None # default value
|
||||
class handler(SentryMixin, requestHelper.asyncRequestHandler):
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncPost(self):
|
||||
try:
|
||||
# This is not the first packet, send response based on client's request
|
||||
# Packet start position, used to read stacked packets
|
||||
pos = 0
|
||||
# Track time if needed
|
||||
if glob.outputRequestTime == True:
|
||||
# Start time
|
||||
st = datetime.datetime.now()
|
||||
|
||||
# Make sure the token exists
|
||||
if requestTokenString not in glob.tokens.tokens:
|
||||
raise exceptions.tokenNotFoundException()
|
||||
# Client's token string and request data
|
||||
requestTokenString = self.request.headers.get("osu-token")
|
||||
requestData = self.request.body
|
||||
|
||||
# Token exists, get its object and lock it
|
||||
userToken = glob.tokens.tokens[requestTokenString]
|
||||
userToken.lock.acquire()
|
||||
# Server's token string and request data
|
||||
responseTokenString = "ayy"
|
||||
responseData = bytes()
|
||||
|
||||
# Keep reading packets until everything has been read
|
||||
while pos < len(requestData):
|
||||
# Get packet from stack starting from new packet
|
||||
leftData = requestData[pos:]
|
||||
if requestTokenString == None:
|
||||
# No token, first request. Handle login.
|
||||
responseTokenString, responseData = loginEvent.handle(self)
|
||||
else:
|
||||
userToken = None # default value
|
||||
try:
|
||||
# This is not the first packet, send response based on client's request
|
||||
# Packet start position, used to read stacked packets
|
||||
pos = 0
|
||||
|
||||
# Get packet ID, data length and data
|
||||
packetID = packetHelper.readPacketID(leftData)
|
||||
dataLength = packetHelper.readPacketLength(leftData)
|
||||
packetData = requestData[pos:(pos+dataLength+7)]
|
||||
# Make sure the token exists
|
||||
if requestTokenString not in glob.tokens.tokens:
|
||||
raise exceptions.tokenNotFoundException()
|
||||
|
||||
# Console output if needed
|
||||
if glob.outputPackets == True and packetID != 4:
|
||||
log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData)))
|
||||
# Token exists, get its object and lock it
|
||||
userToken = glob.tokens.tokens[requestTokenString]
|
||||
userToken.lock.acquire()
|
||||
|
||||
# Event handler
|
||||
def handleEvent(ev):
|
||||
def wrapper():
|
||||
ev.handle(userToken, packetData)
|
||||
return wrapper
|
||||
# Keep reading packets until everything has been read
|
||||
while pos < len(requestData):
|
||||
# Get packet from stack starting from new packet
|
||||
leftData = requestData[pos:]
|
||||
|
||||
eventHandler = {
|
||||
# TODO: Rename packets and events
|
||||
# TODO: Host check for multi
|
||||
packetIDs.client_changeAction: handleEvent(changeActionEvent),
|
||||
packetIDs.client_logout: handleEvent(logoutEvent),
|
||||
packetIDs.client_friendAdd: handleEvent(friendAddEvent),
|
||||
packetIDs.client_friendRemove: handleEvent(friendRemoveEvent),
|
||||
packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent),
|
||||
packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent),
|
||||
packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent),
|
||||
|
||||
packetIDs.client_channelJoin: handleEvent(channelJoinEvent),
|
||||
packetIDs.client_channelPart: handleEvent(channelPartEvent),
|
||||
packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent),
|
||||
packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent),
|
||||
packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent),
|
||||
# Get packet ID, data length and data
|
||||
packetID = packetHelper.readPacketID(leftData)
|
||||
dataLength = packetHelper.readPacketLength(leftData)
|
||||
packetData = requestData[pos:(pos+dataLength+7)]
|
||||
|
||||
packetIDs.client_startSpectating: handleEvent(startSpectatingEvent),
|
||||
packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent),
|
||||
packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent),
|
||||
packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent),
|
||||
# Console output if needed
|
||||
if glob.outputPackets == True and packetID != 4:
|
||||
log.debug("Incoming packet ({})({}):\n\nPacket code: {}\nPacket length: {}\nSingle packet data: {}\n".format(requestTokenString, userToken.username, str(packetID), str(dataLength), str(packetData)))
|
||||
|
||||
packetIDs.client_joinLobby: handleEvent(joinLobbyEvent),
|
||||
packetIDs.client_partLobby: handleEvent(partLobbyEvent),
|
||||
packetIDs.client_createMatch: handleEvent(createMatchEvent),
|
||||
packetIDs.client_joinMatch: handleEvent(joinMatchEvent),
|
||||
packetIDs.client_partMatch: handleEvent(partMatchEvent),
|
||||
packetIDs.client_matchChangeSlot: handleEvent(changeSlotEvent),
|
||||
packetIDs.client_matchChangeSettings: handleEvent(changeMatchSettingsEvent),
|
||||
packetIDs.client_matchChangePassword: handleEvent(changeMatchPasswordEvent),
|
||||
packetIDs.client_matchChangeMods: handleEvent(changeMatchModsEvent),
|
||||
packetIDs.client_matchReady: handleEvent(matchReadyEvent),
|
||||
packetIDs.client_matchNotReady: handleEvent(matchReadyEvent),
|
||||
packetIDs.client_matchLock: handleEvent(matchLockEvent),
|
||||
packetIDs.client_matchStart: handleEvent(matchStartEvent),
|
||||
packetIDs.client_matchLoadComplete: handleEvent(matchPlayerLoadEvent),
|
||||
packetIDs.client_matchSkipRequest: handleEvent(matchSkipEvent),
|
||||
packetIDs.client_matchScoreUpdate: handleEvent(matchFramesEvent),
|
||||
packetIDs.client_matchComplete: handleEvent(matchCompleteEvent),
|
||||
packetIDs.client_matchNoBeatmap: handleEvent(matchNoBeatmapEvent),
|
||||
packetIDs.client_matchHasBeatmap: handleEvent(matchHasBeatmapEvent),
|
||||
packetIDs.client_matchTransferHost: handleEvent(matchTransferHostEvent),
|
||||
packetIDs.client_matchFailed: handleEvent(matchFailedEvent),
|
||||
packetIDs.client_matchChangeTeam: handleEvent(matchChangeTeamEvent),
|
||||
packetIDs.client_invite: handleEvent(matchInviteEvent),
|
||||
}
|
||||
# Event handler
|
||||
def handleEvent(ev):
|
||||
def wrapper():
|
||||
ev.handle(userToken, packetData)
|
||||
return wrapper
|
||||
|
||||
# Packets processed if in restricted mode.
|
||||
# All other packets will be ignored if the user is in restricted mode
|
||||
packetsRestricted = [
|
||||
packetIDs.client_logout,
|
||||
packetIDs.client_userStatsRequest,
|
||||
packetIDs.client_requestStatusUpdate,
|
||||
packetIDs.client_userPanelRequest,
|
||||
packetIDs.client_changeAction,
|
||||
packetIDs.client_channelJoin,
|
||||
packetIDs.client_channelPart,
|
||||
]
|
||||
eventHandler = {
|
||||
# TODO: Rename packets and events
|
||||
# TODO: Host check for multi
|
||||
packetIDs.client_changeAction: handleEvent(changeActionEvent),
|
||||
packetIDs.client_logout: handleEvent(logoutEvent),
|
||||
packetIDs.client_friendAdd: handleEvent(friendAddEvent),
|
||||
packetIDs.client_friendRemove: handleEvent(friendRemoveEvent),
|
||||
packetIDs.client_userStatsRequest: handleEvent(userStatsRequestEvent),
|
||||
packetIDs.client_requestStatusUpdate: handleEvent(requestStatusUpdateEvent),
|
||||
packetIDs.client_userPanelRequest: handleEvent(userPanelRequestEvent),
|
||||
|
||||
packetIDs.client_channelJoin: handleEvent(channelJoinEvent),
|
||||
packetIDs.client_channelPart: handleEvent(channelPartEvent),
|
||||
packetIDs.client_sendPublicMessage: handleEvent(sendPublicMessageEvent),
|
||||
packetIDs.client_sendPrivateMessage: handleEvent(sendPrivateMessageEvent),
|
||||
packetIDs.client_setAwayMessage: handleEvent(setAwayMessageEvent),
|
||||
|
||||
# Process/ignore packet
|
||||
if packetID != 4:
|
||||
if packetID in eventHandler:
|
||||
if userToken.restricted == False or (userToken.restricted == True and packetID in packetsRestricted):
|
||||
eventHandler[packetID]()
|
||||
else:
|
||||
log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID))
|
||||
else:
|
||||
log.warning("Unknown packet id from {} ({})".format(requestTokenString, packetID))
|
||||
packetIDs.client_startSpectating: handleEvent(startSpectatingEvent),
|
||||
packetIDs.client_stopSpectating: handleEvent(stopSpectatingEvent),
|
||||
packetIDs.client_cantSpectate: handleEvent(cantSpectateEvent),
|
||||
packetIDs.client_spectateFrames: handleEvent(spectateFramesEvent),
|
||||
|
||||
# Update pos so we can read the next stacked packet
|
||||
# +7 because we add packet ID bytes, unused byte and data length bytes
|
||||
pos += dataLength+7
|
||||
packetIDs.client_joinLobby: handleEvent(joinLobbyEvent),
|
||||
packetIDs.client_partLobby: handleEvent(partLobbyEvent),
|
||||
packetIDs.client_createMatch: handleEvent(createMatchEvent),
|
||||
packetIDs.client_joinMatch: handleEvent(joinMatchEvent),
|
||||
packetIDs.client_partMatch: handleEvent(partMatchEvent),
|
||||
packetIDs.client_matchChangeSlot: handleEvent(changeSlotEvent),
|
||||
packetIDs.client_matchChangeSettings: handleEvent(changeMatchSettingsEvent),
|
||||
packetIDs.client_matchChangePassword: handleEvent(changeMatchPasswordEvent),
|
||||
packetIDs.client_matchChangeMods: handleEvent(changeMatchModsEvent),
|
||||
packetIDs.client_matchReady: handleEvent(matchReadyEvent),
|
||||
packetIDs.client_matchNotReady: handleEvent(matchReadyEvent),
|
||||
packetIDs.client_matchLock: handleEvent(matchLockEvent),
|
||||
packetIDs.client_matchStart: handleEvent(matchStartEvent),
|
||||
packetIDs.client_matchLoadComplete: handleEvent(matchPlayerLoadEvent),
|
||||
packetIDs.client_matchSkipRequest: handleEvent(matchSkipEvent),
|
||||
packetIDs.client_matchScoreUpdate: handleEvent(matchFramesEvent),
|
||||
packetIDs.client_matchComplete: handleEvent(matchCompleteEvent),
|
||||
packetIDs.client_matchNoBeatmap: handleEvent(matchNoBeatmapEvent),
|
||||
packetIDs.client_matchHasBeatmap: handleEvent(matchHasBeatmapEvent),
|
||||
packetIDs.client_matchTransferHost: handleEvent(matchTransferHostEvent),
|
||||
packetIDs.client_matchFailed: handleEvent(matchFailedEvent),
|
||||
packetIDs.client_matchChangeTeam: handleEvent(matchChangeTeamEvent),
|
||||
packetIDs.client_invite: handleEvent(matchInviteEvent),
|
||||
}
|
||||
|
||||
# Token queue built, send it
|
||||
responseTokenString = userToken.token
|
||||
responseData = userToken.queue
|
||||
userToken.resetQueue()
|
||||
# Packets processed if in restricted mode.
|
||||
# All other packets will be ignored if the user is in restricted mode
|
||||
packetsRestricted = [
|
||||
packetIDs.client_logout,
|
||||
packetIDs.client_userStatsRequest,
|
||||
packetIDs.client_requestStatusUpdate,
|
||||
packetIDs.client_userPanelRequest,
|
||||
packetIDs.client_changeAction,
|
||||
packetIDs.client_channelJoin,
|
||||
packetIDs.client_channelPart,
|
||||
]
|
||||
|
||||
# Update ping time for timeout
|
||||
userToken.updatePingTime()
|
||||
except exceptions.tokenNotFoundException:
|
||||
# Token not found. Disconnect that user
|
||||
responseData = serverPackets.loginError()
|
||||
responseData += serverPackets.notification("Whoops! Something went wrong, please login again.")
|
||||
log.warning("Received packet from unknown token ({}).".format(requestTokenString))
|
||||
log.info("{} has been disconnected (invalid token)".format(requestTokenString))
|
||||
finally:
|
||||
# Unlock token
|
||||
if userToken != None:
|
||||
userToken.lock.release()
|
||||
# Process/ignore packet
|
||||
if packetID != 4:
|
||||
if packetID in eventHandler:
|
||||
if userToken.restricted == False or (userToken.restricted == True and packetID in packetsRestricted):
|
||||
eventHandler[packetID]()
|
||||
else:
|
||||
log.warning("Ignored packet id from {} ({}) (user is restricted)".format(requestTokenString, packetID))
|
||||
else:
|
||||
log.warning("Unknown packet id from {} ({})".format(requestTokenString, packetID))
|
||||
|
||||
if glob.outputRequestTime == True:
|
||||
# End time
|
||||
et = datetime.datetime.now()
|
||||
# Update pos so we can read the next stacked packet
|
||||
# +7 because we add packet ID bytes, unused byte and data length bytes
|
||||
pos += dataLength+7
|
||||
|
||||
# Total time:
|
||||
tt = float((et.microsecond-st.microsecond)/1000)
|
||||
log.debug("Request time: {}ms".format(tt))
|
||||
# Token queue built, send it
|
||||
responseTokenString = userToken.token
|
||||
responseData = userToken.queue
|
||||
userToken.resetQueue()
|
||||
|
||||
# Send server's response to client
|
||||
# We don't use token object because we might not have a token (failed login)
|
||||
if glob.gzip == True:
|
||||
# First, write the gzipped response
|
||||
responseData = gzip.compress(responseData, int(glob.conf.config["server"]["gziplevel"]))
|
||||
# Update ping time for timeout
|
||||
userToken.updatePingTime()
|
||||
except exceptions.tokenNotFoundException:
|
||||
# Token not found. Disconnect that user
|
||||
responseData = serverPackets.loginError()
|
||||
responseData += serverPackets.notification("Whoops! Something went wrong, please login again.")
|
||||
log.warning("Received packet from unknown token ({}).".format(requestTokenString))
|
||||
log.info("{} has been disconnected (invalid token)".format(requestTokenString))
|
||||
finally:
|
||||
# Unlock token
|
||||
if userToken != None:
|
||||
userToken.lock.release()
|
||||
|
||||
# Then, add gzip headers
|
||||
bottle.response.add_header("Vary", "Accept-Encoding")
|
||||
bottle.response.add_header("Content-Encoding", "gzip")
|
||||
else:
|
||||
# First, write the response
|
||||
responseData = responseData
|
||||
if glob.outputRequestTime == True:
|
||||
# End time
|
||||
et = datetime.datetime.now()
|
||||
|
||||
# Add all the headers AFTER the response has been written
|
||||
bottle.response.status = 200
|
||||
bottle.response.add_header("cho-token", responseTokenString)
|
||||
bottle.response.add_header("cho-protocol", "19")
|
||||
bottle.response.add_header("Content-Type", "text/html; charset=UTF-8")
|
||||
yield responseData
|
||||
# Total time:
|
||||
tt = float((et.microsecond-st.microsecond)/1000)
|
||||
log.debug("Request time: {}ms".format(tt))
|
||||
|
||||
@bottle.route("/", method="GET")
|
||||
def GETMain():
|
||||
html = "<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>© Ripple team, 2016</i></pre></body></html>"
|
||||
yield html
|
||||
# Send server's response to client
|
||||
# We don't use token object because we might not have a token (failed login)
|
||||
if glob.gzip == True:
|
||||
# First, write the gzipped response
|
||||
self.write(gzip.compress(responseData, int(glob.conf.config["server"]["gziplevel"])))
|
||||
|
||||
# Then, add gzip headers
|
||||
self.add_header("Vary", "Accept-Encoding")
|
||||
self.add_header("Content-Encoding", "gzip")
|
||||
else:
|
||||
# First, write the response
|
||||
self.write(responseData)
|
||||
|
||||
# Add all the headers AFTER the response has been written
|
||||
self.set_status(200)
|
||||
self.add_header("cho-token", responseTokenString)
|
||||
self.add_header("cho-protocol", "19")
|
||||
#self.add_header("Keep-Alive", "timeout=5, max=100")
|
||||
#self.add_header("Connection", "keep-alive")
|
||||
self.add_header("Content-Type", "text/html; charset=UTF-8")
|
||||
except:
|
||||
log.error("Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
|
||||
if glob.sentry:
|
||||
yield tornado.gen.Task(self.captureException, exc_info=True)
|
||||
#finally:
|
||||
# self.finish()
|
||||
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def asyncGet(self):
|
||||
html = "<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>© Ripple team, 2016</i></pre></body></html>"
|
||||
self.write(html)
|
||||
#yield tornado.gen.Task(self.captureMessage, "test")
|
||||
#self.finish()
|
|
@ -1,7 +1,89 @@
|
|||
import tornado
|
||||
import tornado.web
|
||||
import tornado.gen
|
||||
from tornado.ioloop import IOLoop
|
||||
from objects import glob
|
||||
from raven.contrib.tornado import SentryMixin
|
||||
from raven.contrib.tornado import AsyncSentryClient
|
||||
import gevent
|
||||
|
||||
def getRequestIP(bottleRequest):
|
||||
realIP = bottleRequest.headers.get("X-Forwarded-For") if glob.cloudflare == True else bottleRequest.headers.get("X-Real-IP")
|
||||
if realIP != None:
|
||||
return realIP
|
||||
return bottleRequest.environ.get("REMOTE_ADDR")
|
||||
class asyncRequestHandler(tornado.web.RequestHandler):
|
||||
"""
|
||||
Tornado asynchronous request handler
|
||||
create a class that extends this one (requestHelper.asyncRequestHandler)
|
||||
use asyncGet() and asyncPost() instad of get() and post().
|
||||
Done. I'm not kidding.
|
||||
"""
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def get(self, *args, **kwargs):
|
||||
try:
|
||||
yield tornado.gen.Task(runBackground, (self.asyncGet, tuple(args), dict(kwargs)))
|
||||
except Exception as e:
|
||||
yield tornado.gen.Task(self.captureException, exc_info=True)
|
||||
finally:
|
||||
if not self._finished:
|
||||
self.finish()
|
||||
|
||||
@tornado.web.asynchronous
|
||||
@tornado.gen.engine
|
||||
def post(self, *args, **kwargs):
|
||||
try:
|
||||
yield tornado.gen.Task(runBackground, (self.asyncPost, tuple(args), dict(kwargs)))
|
||||
except Exception as e:
|
||||
yield tornado.gen.Task(self.captureException, exc_info=True)
|
||||
finally:
|
||||
if not self._finished:
|
||||
self.finish()
|
||||
|
||||
def asyncGet(self, *args, **kwargs):
|
||||
self.send_error(405)
|
||||
self.finish()
|
||||
|
||||
def asyncPost(self, *args, **kwargs):
|
||||
self.send_error(405)
|
||||
self.finish()
|
||||
|
||||
def getRequestIP(self):
|
||||
realIP = self.request.headers.get("X-Forwarded-For") if glob.cloudflare == True else self.request.headers.get("X-Real-IP")
|
||||
if realIP != None:
|
||||
return realIP
|
||||
return self.request.remote_ip
|
||||
|
||||
|
||||
def runBackground(data, callback):
|
||||
"""
|
||||
Run a function in the background.
|
||||
Used to handle multiple requests at the same time
|
||||
"""
|
||||
func, args, kwargs = data
|
||||
def _callback(result):
|
||||
IOLoop.instance().add_callback(lambda: callback(result))
|
||||
#glob.pool.apply_async(func, args, kwargs, _callback)
|
||||
g = gevent.Greenlet(func, *args, **kwargs)
|
||||
g.link(_callback)
|
||||
g.start()
|
||||
|
||||
def checkArguments(arguments, requiredArguments):
|
||||
"""
|
||||
Check that every requiredArguments elements are in arguments
|
||||
|
||||
arguments -- full argument list, from tornado
|
||||
requiredArguments -- required arguments list es: ["u", "ha"]
|
||||
handler -- handler string name to print in exception. Optional
|
||||
return -- True if all arguments are passed, none if not
|
||||
"""
|
||||
for i in requiredArguments:
|
||||
if i not in arguments:
|
||||
return False
|
||||
return True
|
||||
|
||||
def printArguments(t):
|
||||
"""
|
||||
Print passed arguments, for debug purposes
|
||||
|
||||
t -- tornado object (self)
|
||||
"""
|
||||
print("ARGS::")
|
||||
for i in t.request.arguments:
|
||||
print ("{}={}".format(i, t.get_argument(i)))
|
54
pep.py
54
pep.py
|
@ -3,11 +3,17 @@ import sys
|
|||
import os
|
||||
import threading
|
||||
|
||||
# Bottle
|
||||
import bottle
|
||||
# Tornado
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import tornado.httpserver
|
||||
import tornado.gen
|
||||
from gevent import monkey as brit_monkey
|
||||
brit_monkey.patch_all()
|
||||
|
||||
# Raven
|
||||
from raven.contrib.tornado import AsyncSentryClient
|
||||
|
||||
# pep.py files
|
||||
from constants import bcolors
|
||||
from helpers import configHelper
|
||||
|
@ -20,13 +26,27 @@ from helpers import databaseHelperNew
|
|||
from helpers import generalFunctions
|
||||
from helpers import logHelper as log
|
||||
|
||||
# Raven
|
||||
from raven import Client
|
||||
from raven.contrib.bottle import Sentry
|
||||
from handlers import mainHandler
|
||||
from handlers import apiIsOnlineHandler
|
||||
from handlers import apiOnlineUsersHandler
|
||||
from handlers import apiServerStatusHandler
|
||||
from handlers import ciTriggerHandler
|
||||
from handlers import apiVerifiedStatusHandler
|
||||
from handlers import apiFokabotMessageHandler
|
||||
|
||||
# IRC
|
||||
from irc import ircserver
|
||||
|
||||
def make_app():
|
||||
return tornado.web.Application([
|
||||
(r"/", mainHandler.handler),
|
||||
(r"/api/v1/isOnline", apiIsOnlineHandler.handler),
|
||||
(r"/api/v1/onlineUsers", apiOnlineUsersHandler.handler),
|
||||
(r"/api/v1/serverStatus", apiServerStatusHandler.handler),
|
||||
(r"/api/v1/ciTrigger", ciTriggerHandler.handler),
|
||||
(r"/api/v1/verifiedStatus", apiVerifiedStatusHandler.handler),
|
||||
(r"/api/v1/fokabotMessage", apiFokabotMessageHandler.handler)
|
||||
])
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Server start
|
||||
consoleHelper.printServerStartHeader(True)
|
||||
|
@ -140,22 +160,13 @@ if __name__ == "__main__":
|
|||
consoleHelper.printColored("[!] Warning! Server running in debug mode!", bcolors.YELLOW)
|
||||
|
||||
# Make app
|
||||
app = bottle.app()
|
||||
app.catchall = False
|
||||
from handlers import mainHandler
|
||||
from handlers import apiIsOnlineHandler
|
||||
from handlers import apiOnlineUsersHandler
|
||||
from handlers import apiServerStatusHandler
|
||||
from handlers import ciTriggerHandler
|
||||
from handlers import apiVerifiedStatusHandler
|
||||
from handlers import apiFokabotMessageHandler
|
||||
application = make_app()
|
||||
|
||||
# Set up sentry
|
||||
try:
|
||||
glob.sentry = generalFunctions.stringToBool(glob.conf.config["sentry"]["enable"])
|
||||
if glob.sentry == True:
|
||||
client = Client(glob.conf.config["sentry"]["banchodns"], release=glob.VERSION)
|
||||
app = Sentry(app, client)
|
||||
application.sentry_client = AsyncSentryClient(glob.conf.config["sentry"]["banchodns"], release=glob.VERSION)
|
||||
else:
|
||||
consoleHelper.printColored("[!] Warning! Sentry logging is disabled!", bcolors.YELLOW)
|
||||
except:
|
||||
|
@ -185,8 +196,9 @@ if __name__ == "__main__":
|
|||
consoleHelper.printColored("[!] Invalid server port! Please check your config.ini and run the server again", bcolors.RED)
|
||||
|
||||
# Server start message and console output
|
||||
log.logMessage("Server started!", discord=True, stdout=False)
|
||||
consoleHelper.printColored("> Bottle listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN)
|
||||
log.logMessage("Server started!", discord=True, of="info.txt", stdout=False)
|
||||
consoleHelper.printColored("> Tornado listening for HTTP(s) clients on 127.0.0.1:{}...".format(serverPort), bcolors.GREEN)
|
||||
|
||||
# Start bottle
|
||||
bottle.run(app=app, host="0.0.0.0", port=serverPort, server="gevent")
|
||||
# Start tornado
|
||||
application.listen(serverPort)
|
||||
tornado.ioloop.IOLoop.instance().start()
|
Loading…
Reference in New Issue
Block a user