async def alert(p: 'Player', c: Messageable, msg: Sequence[str]) -> str: """Send a notification to all players.""" if len(msg) < 1: return 'Invalid syntax: !alert <msg>' glob.players.enqueue(packets.notification(' '.join(msg))) return 'Alert sent.'
async def donor_expiry() -> list[Coroutine]: """Add new donation ranks & enqueue tasks to remove current ones.""" # TODO: this system can get quite a bit better; rather than just # removing, it should rather update with the new perks (potentially # a different tier, enqueued after their current perks). async def rm_donor(userid: int, when: int): if (delta := when - time.time()) >= 0: await asyncio.sleep(delta) p = await glob.players.get_ensure(id=userid) # TODO: perhaps make a `revoke_donor` method? await p.remove_privs(Privileges.Donator) await glob.db.execute( 'UPDATE users ' 'SET donor_end = 0 ' 'WHERE id = %s', [p.id] ) if p.online: p.enqueue(packets.notification('Your supporter status has expired.')) log(f"{p}'s supporter status has expired.", Ansi.LMAGENTA)
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 _remove_expired_donation_privileges(interval: int) -> None: """Remove donation privileges from users with expired sessions.""" while True: if glob.app.debug: log("Removing expired donation privileges.", Ansi.LMAGENTA) expired_donors = await glob.db.fetchall( "SELECT id FROM users " "WHERE donor_end <= UNIX_TIMESTAMP() " "AND priv & 48", # 48 = Supporter | Premium _dict=False, ) for expired_donor_id in expired_donors: p = await glob.players.from_cache_or_sql(id=expired_donor_id) # TODO: perhaps make a `revoke_donor` method? await p.remove_privs(Privileges.DONATOR) await glob.db.execute( "UPDATE users SET donor_end = 0 WHERE id = %s", [p.id], ) if p.online: p.enqueue( packets.notification("Your supporter status has expired.")) log(f"{p}'s supporter status has expired.", Ansi.LMAGENTA) await asyncio.sleep(interval)
class MatchJoin(BanchoPacket, type=Packets.OSU_JOIN_MATCH): match_id: osuTypes.i32 match_passwd: osuTypes.string async def handle(self, p: Player) -> None: if not 0 <= self.match_id < 64: if self.match_id >= 64: # NOTE: this function is unrelated to mp. await check_menu_option(p, self.match_id) p.enqueue(packets.matchJoinFail()) return if not (m := glob.matches[self.match_id]): log(f'{p} tried to join a non-existant mp lobby?') p.enqueue(packets.matchJoinFail()) return if p.silenced: p.enqueue(packets.matchJoinFail() + packets.notification( 'Multiplayer is not available while silenced.')) return await p.update_latest_activity() p.join_match(m, self.match_passwd)
async def handle(self, p: Player) -> None: # TODO: match validation..? if p.silenced: p.enqueue(packets.matchJoinFail() + packets.notification( 'Multiplayer is not available while silenced.')) return if not glob.matches.append(self.match): # failed to create match (match slots full). p.send('Failed to create match (no slots available).', sender=glob.bot) p.enqueue(packets.matchJoinFail()) return # create the channel and add it # to the global channel list as # an instanced channel. chan = Channel(name=f'#multi_{self.match.id}', topic=f"MID {self.match.id}'s multiplayer channel.", auto_join=False, instance=True) glob.channels.append(chan) self.match.chat = chan await p.update_latest_activity() p.join_match(self.match, self.match.passwd) log(f'{p} created a new multiplayer match.')
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 asyncio.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 = 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. # 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''
def restrict(self) -> None: # TODO: reason self.priv &= ~Privileges.Normal glob.db.execute('UPDATE users SET priv = %s WHERE id = %s', [int(self.priv), self.id]) if self in glob.players: # If user is online, notify and log them out. # XXX: If you want to lock the player's # client, you can send -3 rather than -1. self.enqueue(packets.userID(-1)) self.enqueue( packets.notification('Your account has been banned.\n\n' 'If you believe this was a mistake or ' 'have waited >= 2 months, you can appeal ' 'using the appeal form on the website.')) printlog(f'Restricted {self}.', Ansi.CYAN)
async def before_serving() -> None: """Called before the server begins serving connections.""" # retrieve a client session to use for http connections. glob.http = aiohttp.ClientSession(json_serialize=orjson.dumps) # retrieve a pool of connections to use for mysql interaction. glob.db = cmyui.AsyncSQLPool() await glob.db.connect(glob.config.mysql) # run the sql & submodule updater (uses http & db). updater = Updater(glob.version) await updater.run() await updater.log_startup() # cache many global collections/objects from sql, # such as channels, mappools, clans, bot, etc. await setup_collections() # add new donation ranks & enqueue tasks to remove current ones. # TODO: this system can get quite a bit better; rather than just # removing, it should rather update with the new perks (potentially # a different tier, enqueued after their current perks). async def rm_donor(userid: int, when: int): if (delta := when - time.time()) >= 0: await asyncio.sleep(delta) p = await glob.players.get(id=userid, sql=True) # TODO: perhaps make a `revoke_donor` method? await p.remove_privs(Privileges.Donator) await glob.db.execute( 'UPDATE users ' 'SET donor_end = 0 ' 'WHERE id = %s', [p.id]) await glob.db.execute( 'DELETE FROM user_badges WHERE badgeid = 3 AND userid = %s', [p.id]) if p.online: p.enqueue( packets.notification('Your supporter status has expired.')) log(f"{p}'s supporter status has expired.", Ansi.MAGENTA)
async def ban(self, admin: 'Player', reason: str) -> None: self.priv &= ~Privileges.Normal await glob.db.execute('UPDATE users SET priv = %s WHERE id = %s', [int(self.priv), self.id]) log_msg = f'{admin} banned for "{reason}".' await glob.db.execute( 'INSERT INTO logs (`from`, `to`, `msg`, `time`) ' 'VALUES (%s, %s, %s, NOW())', [admin.id, self.id, log_msg]) if self in glob.players: # if user is online, notify and log them out. # XXX: if you want to lock the player's # client, you can send -3 rather than -1. self.enqueue(packets.userID(-1)) self.enqueue( packets.notification('Your account has been banned.\n\n' 'If you believe this was a mistake or ' 'have waited >= 2 months, you can appeal ' 'using the appeal form on the website.')) log(f'Banned {self}.', Ansi.CYAN)
async def lastFM(p: Player, conn: AsyncConnection) -> Optional[bytes]: if conn.args['b'][0] != 'a': # not anticheat related, tell the # client not to send any more for now. return b'-3' flags = ClientFlags(int(conn.args['b'][1:])) if flags & (ClientFlags.HQAssembly | ClientFlags.HQFile): # Player is currently running hq!osu; could possibly # be a separate client, buuuut prooobably not lol. await p.ban(glob.bot, f'hq!osu running ({flags})') return b'-3' if flags & ClientFlags.RegistryEdits: # Player has registry edits left from # hq!osu's multiaccounting tool. This # does not necessarily mean they are # using it now, but they have in the past. if random.randrange(32) == 0: # Random chance (1/32) for a ban. await p.ban(glob.bot, f'hq!osu relife 1/32') return b'-3' p.enqueue(packets.notification('\n'.join([ "Hey!", "It appears you have hq!osu's multiaccounting tool (relife) enabled.", "This tool leaves a change in your registry that the osu! client can detect.", "Please re-install relife and disable the program to avoid possible ban." ]))) await p.logout() return b'-3' """ These checks only worked for ~5 hours from release. rumoi's quick!
class SendPrivateMessage(BanchoPacket, type=Packets.OSU_SEND_PRIVATE_MESSAGE): msg: osuTypes.message async def handle(self, p: Player) -> None: if p.silenced: log(f'{p} tried to send a dm while silenced.', Ansi.LYELLOW) return # remove leading/trailing whitespace msg = self.msg.msg.strip() t_name = self.msg.target # allow this to get from sql - players can receive # messages offline, due to the mail system. B) if not (t := await glob.players.get_ensure(name=t_name)): log(f'{p} tried to write to non-existent user {t_name}.', Ansi.LYELLOW) return if t.pm_private and p.id not in t.friends: p.enqueue(packets.userDMBlocked(t_name)) log(f'{p} tried to message {t}, but they are blocking dms.') return if t.silenced: # if target is silenced, inform player. p.enqueue(packets.targetSilenced(t_name)) log(f'{p} tried to message {t}, but they are silenced.') return # limit message length to 2k chars # perhaps this could be dangerous with !py..? if len(msg) > 2000: msg = f'{msg[:2000]}... (truncated)' p.enqueue( packets.notification('Your message was truncated\n' '(exceeded 2000 characters).')) if t.status.action == Action.Afk and t.away_msg: # send away message if target is afk and has one set. p.send(t.away_msg, sender=t) if t is glob.bot: # may have a command in the message. cmd = (msg.startswith(glob.config.command_prefix) and await commands.process_commands(p, t, msg)) if cmd: # command triggered, send response if any. if 'resp' in cmd: p.send(cmd['resp'], sender=t) else: # no commands triggered. if match := regexes.now_playing.match(msg): # user is /np'ing a map. # save it to their player instance # so we can use this elsewhere owo.. bmap = await Beatmap.from_bid(int(match['bid'])) if bmap: # parse mode_vn int from regex if match['mode_vn'] is not None: mode_vn = { 'Taiko': 1, 'CatchTheBeat': 2, 'osu!mania': 3 }[match['mode_vn']] else: # use beatmap mode if not specified mode_vn = bmap.mode.as_vanilla p.last_np = { 'bmap': bmap, 'mode_vn': mode_vn, 'timeout': time.time() + 300 # 5mins } # calc pp if possible if not glob.oppai_built: msg = 'No oppai-ng binary was found at startup.' elif mode_vn not in (0, 1): msg = 'PP not yet supported for that mode.' else: if match['mods'] is not None: # [1:] to remove leading whitespace mods = Mods.from_np(match['mods'][1:], mode_vn) else: mods = Mods.NOMOD if mods not in bmap.pp_cache: await bmap.cache_pp(mods) # since this is a DM to the bot, we should # send back a list of general PP values. _msg = [bmap.embed] if mods: _msg.append(f'+{mods!r}') msg = f"{' '.join(_msg)}: " + ' | '.join([ f'{acc}%: {pp:.2f}pp' for acc, pp in zip((90, 95, 98, 99, 100), bmap.pp_cache[mods]) ]) else: msg = 'Could not find map.' # time out their previous /np p.last_np['timeout'] = 0 p.send(msg, sender=t)
reader = database.Reader('ext/geoloc.mmdb') geoloc = reader.city(ip) country, user['lat'], user['lon'] = (geoloc.country.iso_code, geoloc.location.latitude, geoloc.location.longitude) user['country'] = country_codes[country] await glob.db.execute('UPDATE users SET country = %s WHERE id = %s', [country.lower(), user['id']]) # update country code in db friends = {row['user2'] async for row in glob.db.iterall('SELECT user2 FROM friends WHERE user1 = %s', [user['id']])} # select all friends from db 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 i guess data += packets.channelInfoEnd() # no clue what this does either data += packets.menuIcon() # set main menu icon data += packets.friends(*friends) # send user friend list data += packets.silenceEnd(0) # force to 0 for now since silences arent a thing #data += packets.sendMessage(user['name'], 'test message lol so cool', user['name'], user['id']) # test message # add user to cache? pcache = glob.players 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:
async def handle(self, p: Player) -> None: if p.silenced: log(f'{p} sent a message while silenced.', Ansi.LYELLOW) return # remove leading/trailing whitespace msg = self.msg.msg.strip() target = self.msg.target if target == '#spectator': if p.spectating: # we are spectating someone spec_id = p.spectating.id elif p.spectators: # we are being spectated spec_id = p.id else: return t_chan = glob.channels[f'#spec_{spec_id}'] elif target == '#multiplayer': if not p.match: # they're not in a match? return t_chan = p.match.chat else: t_chan = glob.channels[target] if not t_chan: log(f'{p} wrote to non-existent {target}.', Ansi.LYELLOW) return if p.priv & t_chan.write_priv != t_chan.write_priv: log(f'{p} wrote to {target} with insufficient privileges.') return # limit message length to 2k chars # perhaps this could be dangerous with !py..? if len(msg) > 2000: msg = f'{msg[:2000]}... (truncated)' p.enqueue( packets.notification('Your message was truncated\n' '(exceeded 2000 characters).')) cmd = (msg.startswith(glob.config.command_prefix) and await commands.process_commands(p, t_chan, msg)) if cmd: # a command was triggered. if not cmd['hidden']: t_chan.send(msg, sender=p) if 'resp' in cmd: t_chan.send_bot(cmd['resp']) else: staff = glob.players.staff t_chan.send_selective(msg=msg, sender=p, targets=staff - {p}) if 'resp' in cmd: t_chan.send_selective(msg=cmd['resp'], sender=glob.bot, targets=staff | {p}) else: # no commands were triggered # check if the user is /np'ing a map. # even though this is a public channel, # we'll update the player's last np stored. if match := regexes.now_playing.match(msg): # the player is /np'ing a map. # save it to their player instance # so we can use this elsewhere owo.. bmap = await Beatmap.from_bid(int(match['bid'])) if bmap: # parse mode_vn int from regex if match['mode_vn'] is not None: mode_vn = { 'Taiko': 1, 'CatchTheBeat': 2, 'osu!mania': 3 }[match['mode_vn']] else: # use beatmap mode if not specified mode_vn = bmap.mode.as_vanilla p.last_np = { 'bmap': bmap, 'mode_vn': mode_vn, 'timeout': time.time() + 300 # 5mins } else: # time out their previous /np p.last_np['timeout'] = 0 t_chan.send(msg, sender=p)
# No specific packetID, triggered when the # client sends a request without an osu-token. def login(origin: bytes, ip: str) -> Tuple[bytes, str]: # Login is a bit special, we return the response bytes # and token in a tuple - we need both for our response. s = origin.decode().split('\n') if p := glob.players.get_by_name(username := s[0]): if (time() - p.ping_time) > 20: # If the current player obj online hasn't # pinged the server in > 20 seconds, log # them out and login the new user. p.logout() else: # User is currently online, send back failure. return packets.notification('User already logged in.') \ + packets.userID(-1), 'no' del p pw_hash = s[1].encode() s = s[2].split('|') build_name = s[0] if not s[1].replace('-', '', 1).isnumeric(): return packets.userID(-1), 'no' utc_offset = int(s[1]) display_city = s[2] == '1'
async def freeze_user(userid: int, when: int): if (delta := when - time.time()) >= 0: await asyncio.sleep(delta) p = await glob.players.get(id=userid, sql=True) await p.ban(glob.bot, 'expired freeze timer') await glob.db.execute( 'UPDATE users ' 'SET frozen = 0 ' 'WHERE id = %s', [p.id]) if p.online: p.enqueue( packets.notification( 'Your freeze timer expired and you did not present a liveplay. You are now banned!' )) log(f"{p}'s freeze timer has ran out and has been banned as a result", Ansi.MAGENTA) # enqueue rm_donor for any supporter # expiring in the next 30 days. query = ( 'SELECT id, donor_end FROM users ' 'WHERE donor_end < DATE_ADD(NOW(), INTERVAL 30 DAY) ' 'AND priv & 48' # 48 = Supporter | Premium ) query2 = ('SELECT id, freezetime FROM users ' 'WHERE freezetime < DATE_ADD(NOW(), INTERVAL 1 DAY) ' 'AND frozen = 1')
pm_private = client_info[4] == '1' """ Parsing complete, now check the given data. """ login_time = time.time() # Check if the player is already online if p := glob.players.get(name=username): if (login_time - p.last_recv_time) > 10: # if the current player obj online hasn't # pinged the server in > 10 seconds, log # them out and login the new user. p.logout() else: # the user is currently online, send back failure. data = packets.userID(-1) + \ packets.notification('User already logged in.') return data, 'no' user_info = await glob.db.fetch( 'SELECT id, name, priv, pw_bcrypt, ' 'silence_end, clan_id, clan_priv ' 'FROM users WHERE safe_name = %s', [make_safe_name(username)]) if not user_info: # no account by this name exists. return packets.userID(-1), 'no' # get our bcrypt cache. bcrypt_cache = glob.cache['bcrypt'] pw_bcrypt = user_info['pw_bcrypt'].encode()
async def login(origin: bytes, ip: str) -> tuple[bytes, str]: # login is a bit special, we return the response bytes # and token in a tuple - we need both for our response. if len(s := origin.decode().split('\n')[:-1]) != 3: return if p := await glob.players.get_by_name(username := s[0]): if (time.time() - p.last_recv_time) > 10: # if the current player obj online hasn't # pinged the server in > 10 seconds, log # them out and login the new user. await p.logout() else: # the user is currently online, send back failure. data = packets.userID(-1) + \ packets.notification('User already logged in.') return data, 'no' del p pw_hash = s[1].encode() if len(s := s[2].split('|')) != 5: return if not (r := regexes.osu_ver.match(s[0])): # invalid client version? return packets.userID(-2), 'no' # parse their osu version into a datetime object.
async def callback(): # this is called when the menu item is clicked p.enqueue(packets.notification('clicked!'))
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)
if len(s := origin.decode().split('\n')[:-1]) != 3: return username = s[0] login_time = time.time() if p := await glob.players.get(name=username): if (login_time - p.last_recv_time) > 10: # if the current player obj online hasn't # pinged the server in > 10 seconds, log # them out and login the new user. await p.logout() else: # the user is currently online, send back failure. data = packets.userID(-1) + \ packets.notification('User already logged in.') return data, 'no' if 'ainu' in headers: if not (t := await glob.players.get(name=username, sql=True)): return f'"{username}" not found.' reason = 'Cheat client found.' await t.ban(p, reason) pw_md5 = s[1].encode() if len(s := s[2].split('|')) != 5: return packets.userID(-2), 'no' e = await glob.db.fetch('SELECT unsupver, banver FROM server_stats')
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
return 'Invalid syntax: !alert <msg>' glob.players.enqueue(packets.notification(' '.join(msg))) return 'Alert sent.' @command(Privileges.Admin, hidden=True) async def alertu(p: 'Player', c: Messageable, msg: Sequence[str]) -> str: """Send a notification to a specified player by name.""" if len(msg) < 2: return 'Invalid syntax: !alertu <name> <msg>' if not (t := await glob.players.get(name=msg[0])): return 'Could not find a user by that name.' t.enqueue(packets.notification(' '.join(msg[1:]))) return 'Alert sent.' """ Developer commands # The commands below are either dangerous or # simply not useful for any other roles. """ @command(Privileges.Dangerous) async def recalc(p: 'Player', c: Messageable, msg: Sequence[str]) -> str: """Performs a full PP recalc on a specified map, or all maps.""" if len(msg) != 1 or msg[0] not in ('map', 'all'): return 'Invalid syntax: !recalc <map/all>'
if flags & ClientFlags.RegistryEdits: # Player has registry edits left from # hq!osu's multiaccounting tool. This # does not necessarily mean they are # using it now, but they have in the past. if randrange(32) == 0: # Random chance (1/32) for a restriction. p.restrict() return p.enqueue( packets.notification('\n'.join([ "Hey!", "It appears you have hq!osu's multiaccounting tool (relife) enabled.", "This tool leaves a change in your registry that the osu! client can detect.", "Please re-install relife and disable the program to avoid possible restriction." ]))) return """ These checks only worked for ~5 hours from release. rumoi's quick! if flags & (ClientFlags.libeay32Library | ClientFlags.aqnMenuSample): # AQN has been detected in the client, either # through the 'libeay32.dll' library being found # onboard, or from the menu sound being played in # the AQN menu while being in an inappropriate menu # for the context of the sound effect. pass """ @unique
def alert(p: Player, c: Messageable, msg: List[str]) -> str: if len(msg) < 1: return 'Invalid syntax: !alert <msg>' glob.players.enqueue(packets.notification(' '.join(msg))) return 'Alert sent.'