def handle_bancho(conn: Connection) -> None: if 'User-Agent' not in conn.req.headers: return if conn.req.headers['User-Agent'] != 'osu!': # Most likely a request from a browser. conn.resp.send( b'<!DOCTYPE html>' + '<br>'.join( (f'Running gulag v{glob.version}', f'Players online: {len(glob.players) - 1}', '<a href="https://github.com/cmyui/gulag">Source code</a>', '', '<b>Bancho Handlers</b>', '<br>'.join( f'{int(x)}: {str(x)[9:]}' for x in glob.bancho_map.keys()), '', '<b>/web/ Handlers</b>', '<br>'.join( glob.web_map.keys()))).encode(), 200) return resp = bytearray() if 'osu-token' not in conn.req.headers: # Login is a bit of a special case, # so we'll handle it separately. login_data = loginEvent(conn.req.body, conn.req.headers['X-Real-IP']) resp.extend(login_data[0]) conn.resp.add_header(f'cho-token: {login_data[1]}') elif not (p := glob.players.get(conn.req.headers['osu-token'])): printlog('Token not found, forcing relog.') resp.extend( packets.notification('Server is restarting.') + packets.restartServer( 0) # send 0ms since the server is already up! )
async def bancho_handler(conn: Connection) -> bytes: if ('User-Agent' not in conn.headers or conn.headers['User-Agent'] != 'osu!'): return # check for 'osu-token' in the headers. # if it's not there, this is a login request. if 'osu-token' not in conn.headers: # login is a bit of a special case, # so we'll handle it separately. async with glob.players._lock: resp, token = await login(conn.body, conn.headers['X-Real-IP'], conn.headers) conn.add_resp_header(f'cho-token: {token}') return resp # get the player from the specified osu token. player = await glob.players.get(token=conn.headers['osu-token']) if not player: # token was not found; changes are, we just restarted # the server. just tell their client to re-connect. return packets.restartServer(0) # bancho connections can be comprised of multiple packets; # our reader is designed to iterate through them individually, # allowing logic to be implemented around the actual handler. # NOTE: the reader will internally discard any # packets whose logic has not been defined. # TODO: why is the packet reader async lol async for packet in BanchoPacketReader(conn.body): await packet.handle(player) if glob.config.debug: log(f'{packet.type!r}', Ansi.LMAGENTA) player.last_recv_time = time.time() # TODO: this could probably be done better? resp = bytearray() while not player.queue_empty(): # read all queued packets into stream resp += player.dequeue() conn.add_resp_header('Content-Type: text/html; charset=UTF-8') resp = bytes(resp) # even if the packet is empty, we have to # send back an empty response so the client # knows it was successfully delivered. return resp
async def bancho_handler(conn: Connection) -> bytes: if ('User-Agent' not in conn.headers or conn.headers['User-Agent'] != 'osu!'): return # check for 'osu-token' in the headers. # if it's not there, this is a login request. if 'osu-token' not in conn.headers: # login is a bit of a special case, # so we'll handle it separately. async with glob.players._lock: resp, token = await login(conn.body, conn.headers['X-Real-IP']) conn.add_resp_header(f'cho-token: {token}') return resp # get the player from the specified osu token. player = glob.players.get(token=conn.headers['osu-token']) if not player: # token was not found; changes are, we just restarted # the server. just tell their client to re-connect. return packets.notification('Server is restarting') + \ packets.restartServer(0) # send 0ms since server is up # restricted users may only use certain packet handlers. if not player.restricted: packet_map = glob.bancho_packets['all'] else: packet_map = glob.bancho_packets['restricted'] # bancho connections can be comprised of multiple packets; # our reader is designed to iterate through them individually, # allowing logic to be implemented around the actual handler. # NOTE: the reader will internally discard any # packets whose logic has not been defined. packets_read = [] for packet in BanchoPacketReader(conn.body, packet_map): await packet.handle(player) packets_read.append(packet.type) if glob.config.debug: packets_str = ', '.join([p.name for p in packets_read]) or 'None' log(f'[BANCHO] {player} | {packets_str}.', AnsiRGB(0xff68ab)) player.last_recv_time = time.time() conn.add_resp_header('Content-Type: text/html; charset=UTF-8') return player.dequeue() or b''
async def bancho_handler(conn: Connection) -> bytes: if 'User-Agent' not in conn.headers: return if conn.headers['User-Agent'] != 'osu!': # most likely a request from a browser. return b'<!DOCTYPE html>' + '<br>'.join(( f'Running gulag v{glob.version}', f'Players online: {len(glob.players) - 1}', '<a href="https://github.com/cmyui/gulag">Source code</a>', '', '<b>Packets handled</b>', '<br>'.join(f'{p.name} ({p.value})' for p in glob.bancho_packets) )).encode() # check for 'osu-token' in the headers. # if it's not there, this is a login request. if 'osu-token' not in conn.headers: # login is a bit of a special case, # so we'll handle it separately. resp, token = await login( conn.body, conn.headers['X-Real-IP'] ) conn.add_resp_header(f'cho-token: {token}') return resp # get the player from the specified osu token. player = await glob.players.get(token=conn.headers['osu-token']) if not player: # token was not found; changes are, we just restarted # the server. just tell their client to re-connect. return packets.notification('Server is restarting') + \ packets.restartServer(0) # send 0ms since server is up # bancho connections can be comprised of multiple packets; # our reader is designed to iterate through them individually, # allowing logic to be implemented around the actual handler. # NOTE: the reader will internally discard any # packets whose logic has not been defined. async for packet in BanchoPacketReader(conn.body): await packet.handle(player) if glob.config.debug: log(f'{packet.type!r}', Ansi.LMAGENTA) player.last_recv_time = time.time() # TODO: this could probably be done better? resp = bytearray() while not player.queue_empty(): # read all queued packets into stream resp += player.dequeue() conn.add_resp_header('Content-Type: text/html; charset=UTF-8') resp = bytes(resp) # even if the packet is empty, we have to # send back an empty response so the client # knows it was successfully delivered. return resp
pcache.append(user) for p in pcache: # enqueue other users to client data += (packets.userPresence(p) + packets.userStats(p)) resp = await make_response(bytes(data)) resp.headers['cho-token'] = token if glob.config.debug: log(f'{username} successfully logged in. | Time Elapsed (using bcrypt cache: {ub}): {(time.time() - start) * 1000:.2f}ms', Ansi.LBLUE) return resp # if we have made it this far then it's a reconnect attempt with token already provided user_token = headers['osu-token'] # client-provided token tcache = glob.cache['user'] # get token/userid cache to see if we need to relog the user or not if user_token not in tcache: # user is logged in but token is not found? most likely a restart so we force a reconnection return packets.restartServer(0) user = await glob.db.fetch('SELECT id, pw, country, name FROM users WHERE id = %s', [tcache[user_token]]) body = await request.body # ew ucache = glob.players for p in ucache: if p.get('id') == user['id']: user = p # handle any packets the client has sent for packet in BanchoPacketReader(body, glob.packets): await packet.handle(user) if glob.config.debug: log(f'Handled packet {packet.type!r}', Ansi.LBLUE)
async def login(): headers = request.headers # request headers, used for things such as user ip and agent if 'User-Agent' not in headers or headers['User-Agent'] != 'osu!': # request isn't sent from osu client, return nothing return if 'osu-token' not in headers: # sometimes a login request will be a re-connect attempt, in which case they will already have a token, if not: login the user data = await request.data # request data, used to get info such as username to login the user info = data.decode().split( '\n')[:-1] # format data so we can use it easier username = info[0] pw = info[1].encode( ) # password in md5 form, we will use this to compare against db's stored bcrypt later user = await glob.db.fetch( 'SELECT id, pw, country, name FROM users WHERE name = %s', [username]) if not user: # ensure user actually exists before attempting to do anything else log(f'User {username} does not exist.', Ansi.LRED) resp = await make_response(packets.userID(-1)) resp.headers['cho-token'] = 'no' return resp bcache = glob.cache[ 'bcrypt'] # get our cached bcrypts to potentially enhance speed pw_bcrypt = user['pw'].encode() if pw_bcrypt in bcache: if pw != bcache[ pw_bcrypt]: # compare provided md5 with the stored (cached) bcrypt to ensure they have provided the correct password log( f"{username}'s login attempt failed: provided an incorrect password", Ansi.LRED) resp = await make_response(packets.userID(-1)) resp.headers['cho-token'] = 'no' return resp else: if not bcrypt.checkpw( pw, pw_bcrypt ): # compare provided md5 with the stored bcrypt to ensure they have provided the correct password log( f"{username}'s login attempt failed: provided an incorrect password", Ansi.LRED) resp = await make_response(packets.userID(-1)) resp.headers['cho-token'] = 'no' return resp bcache[pw_bcrypt] = pw # cache pw for future token = uuid.uuid4() # generate token for client to use as auth ucache = glob.cache['user'] if str(token) not in ucache: ucache[str(token)] = user[ 'id'] # cache token to use outside of this request data = bytearray(packets.userID( user['id'])) # initiate login by providing the user's id data += packets.protocolVersion(19) # no clue what this does data += packets.banchoPrivileges( 1 << 4) # force priv to developer for now data += ( packets.userPresence(user) + packets.userStats(user) ) # provide user & other user's presence/stats (for f9 + user stats) data += packets.notification( f'Welcome to Asahi v{glob.version}' ) # send notification as indicator they've logged in iguess data += packets.channelInfoEnd() # no clue what this does either resp = await make_response(bytes(data)) resp.headers['cho-token'] = token log(f'{username} successfully logged in.', Ansi.GREEN) return resp # if we have made it this far then it's a reconnect attempt with token already provided user_token = headers['osu-token'] # client-provided token tcache = glob.cache[ 'user'] # get token/userid cache to see if we need to relog the user or not if user_token not in tcache: # user is logged in but token is not found? most likely a restart so we force a reconnection return packets.restartServer(0) user = await glob.db.fetch( 'SELECT id, pw, country, name FROM users WHERE id = %s', [tcache[user_token]]) body = await request.body # handle any packets the client has sent | doesn't really work **for now** for packet in BanchoPacketReader(body, glob.packets): await packet.handle(user) log(f'Handled packet {packet}') resp = await make_response(b'') resp.headers['Content-Type'] = 'text/html; charset=UTF-8' # ? return resp
async def handle_bancho(conn: AsyncConnection) -> None: """Handle a bancho request (POST c.ppy.sh/).""" if 'User-Agent' not in conn.headers: return if conn.headers['User-Agent'] != 'osu!': # most likely a request from a browser. resp = '<br>'.join( (f'Running gulag v{glob.version}', f'Players online: {len(glob.players) - 1}', '<a href="https://github.com/cmyui/gulag">Source code</a>', '', '<b>Bancho Handlers</b>', '<br>'.join(f'{h.name} ({h.value})' for h in glob.bancho_map), '', '<b>/web/ Handlers</b>', '<br>'.join(glob.web_map), '', '<b>/api/ Handlers</b>', '<br>'.join(glob.api_map))) await conn.send(200, f'<!DOCTYPE html>{resp}'.encode()) return # check for 'osu-token' in the headers. # if it's not there, this is a login request. if 'osu-token' not in conn.headers: # login is a bit of a special case, # so we'll handle it separately. resp, token = await bancho.login(conn.body, conn.headers['X-Real-IP']) await conn.add_resp_header(f'cho-token: {token}') await conn.send(200, resp) return # get the player from the specified osu token. p = glob.players.get(conn.headers['osu-token']) if not p: # token was not found; changes are, we just restarted # the server. just tell their client to re-connect. resp = packets.notification('Server is restarting') + \ packets.restartServer(0) # send 0ms since server is up await conn.send(200, resp) return # bancho connections can be comprised of multiple packets; # our reader is designed to iterate through them individually, # allowing logic to be implemented around the actual handler. # NOTE: this will internally discard any # packets whose logic has not been defined. async for packet in BanchoPacketReader(conn.body): # TODO: wait_for system here with # a packet and a callable check. await packet.handle(p) if glob.config.debug: log(repr(packet.type), Ansi.LMAGENTA) p.last_recv_time = int(time.time()) # TODO: this could probably be done better? resp = bytearray() while not p.queue_empty(): # read all queued packets into stream resp.extend(p.dequeue()) resp = bytes(resp) # compress with gzip if enabled. if glob.config.gzip['web'] > 0: resp = gzip.compress(resp, glob.config.gzip['web']) await conn.add_resp_header('Content-Encoding: gzip') # add headers and such await conn.add_resp_header('Content-Type: text/html; charset=UTF-8') # even if the packet is empty, we have to # send back an empty response so the client # knows it was successfully delivered. await conn.send(200, resp)