async def parse_country(self, ip: str) -> bool: if self.privileges & Privileges.USER_DONOR: # we need to remember donor have locked location donor_location: str = (await Context.mysql.fetch( 'select country from users_stats where id = %s', [self.id]))['country'].upper() self.country = (Countries.get_country_id(donor_location), donor_location) else: data = None async with aiohttp.ClientSession() as sess: async with sess.get(Config.config['geoloc_ip'] + ip) as resp: try: data = await resp.json() finally: pass if not data: logger.elog(f"[Player/{self.name}] Can't parse geoloc") return False self.country = (Countries.get_country_id(data['country']), data['country']) loc = data['loc'].split(",") self.location = (float(loc[0]), float(loc[1])) return True
async def add_spectator(self, new_spec: 'Player') -> bool: spec_chan_name = f"#spec_{self.id}" if not Context.channels.get(spec_chan_name): # in this case, we need to create channel for our spectator in temp mode spec = Channel(server_name=spec_chan_name, description=f"Spectator channel for {self.name}", public_read=True, public_write=True, temp_channel=True) Context.channels[spec_chan_name] = spec await spec.join_channel(self) c: 'Channel' = Context.channels.get(spec_chan_name) if not await c.join_channel(new_spec): logger.elog( f"{self.name} failed to join in {spec_chan_name} spectator channel!" ) return False fellow_packet = await PacketBuilder.FellowSpectatorJoined(new_spec.id) for spectator in self.spectators: spectator.enqueue(fellow_packet) new_spec.enqueue(await PacketBuilder.FellowSpectatorJoined(spectator.id)) self.spectators.append(new_spec) new_spec.spectating = self self.enqueue(await PacketBuilder.SpectatorJoined(new_spec.id)) logger.slog(f"{new_spec.name} started to spectating {self.name}!") return True
async def presence_update(packet_data: bytes, p: 'Player'): data = await PacketResolver.read_pr_filter(packet_data) if not 0 <= data < 3: logger.elog(f"[Player/{p.name}] Tried to set bad pr filter") return p.presence_filter = PresenceFilter(data)
async def activate_user(user_id: int, user_name: str, hashes: Union[Tuple[str], List[str]]) -> bool: if len(hashes) < 5 or not all((x for x in hashes)): logger.elog( f"[Verification/{user_id}] have wrong hash set! Probably generated by randomizer" ) return False match: dict if hashes[2] == "b4ec3c4334a0249dae95c284ec5983df" or \ hashes[4] == "ffae06fb022871fe9beb58b005c5e21d": # user logins from wine(old bancho checks) match = await Context.mysql.fetch( "select userid from hw_user where unique_id = %(unique_id)s and userid != %(userid)s and activated = 1 limit 1", { 'unique_id': hashes[3], 'userid': user_id }) else: # its 100%(prob 80%) windows match = await Context.mysql.fetch( 'select userid from hw_user ' 'where mac = %(mac)s and unique_id = %(unique_id)s ' 'and disk_id = %(disk_id)s ' 'and userid != %(userid)s ' 'and activated = 1 LIMIT 1', { "mac": hashes[2], "unique_id": hashes[3], "disk_id": hashes[4], "userid": user_id }) await Context.mysql.execute( 'update users set privileges = privileges & %s where id = %s limit 1', [~Privileges.USER_PENDING_VERIFICATION, user_id]) if match: source_user_id = match['userid'] source_user_name = (await get_username(source_user_id)) # баним его await ban(source_user_id) # уведомляем стафф, что это читерюга и как-бы ну нафиг. await append_notes(user_id, [ f"{source_user_name}\'s multiaccount ({hashes[2:5]}),found HWID match while verifying account ({user_id})" ]) await append_notes( source_user_id, [f"Has created multiaccount {user_name} ({user_id})"]) logger.klog( f"[{source_user_name}] Has created multiaccount {user_name} ({user_id})" ) return False await Context.mysql.execute( "UPDATE users SET privileges = privileges | %s WHERE id = %s LIMIT 1", [(Privileges.USER_PUBLIC | Privileges.USER_NORMAL), user_id]) return True
async def channel_join(packet_data: bytes, token: 'Player'): chan_name = await PacketResolver.read_channel_name(packet_data) if not chan_name.startswith("#"): return chan: 'Channel' = Context.channels.get(chan_name, None) if not chan: logger.elog(f'[{token.name}] Failed to join in {chan_name}') return False await chan.join_channel(token) return True
async def leave_spectator(_, token: 'Player'): old_victim = token.spectating if not old_victim: logger.elog( f"{token.name} tried to stop spectating empty old victim...") return False # because this is bug and impossible in context if token.is_tourneymode: await old_victim.remove_hidden_spectator(token) else: await old_victim.remove_spectator(token) return True
async def cant_spectate(_, token: 'Player'): if not token.spectating: logger.elog( f"{token.name} sent that he can't spectate, but he is not spectating..." ) return False # impossible condition packet = await PacketBuilder.CantSpectate(token.id) token.spectating.enqueue(packet) # send this sweet packet lol for recv in token.spectating.spectators: recv.enqueue(packet) return True
async def update_stats(self, selected_mode: GameModes = None) -> bool: for mode in GameModes if not selected_mode else [selected_mode]: res = await Context.mysql.fetch( 'select total_score_{0} as total_score, ranked_score_{0} as ranked_score, ' 'pp_{0} as pp, playcount_{0} as total_plays, avg_accuracy_{0} as accuracy, playtime_{0} as playtime ' 'from users_stats where id = %s'.format( GameModes.resolve_to_str(mode)), [self.id]) if not res: logger.elog( f"[Player/{self.name}] Can't parse stats for {GameModes.resolve_to_str(mode)}" ) return False res['leaderboard_rank'] = 0 self.stats[mode].update(**res) return True
async def join_spectator(packet_data: bytes, token: 'Player'): to_spectate_id = await PacketResolver.read_specatator_id(packet_data) player_spec = Context.players.get_token(uid=to_spectate_id) if not player_spec: logger.elog( f"{token.name} failed to spectate non-exist user with id {to_spectate_id}" ) return False if player_spec: token.enqueue(await PacketBuilder.UserStats(player_spec) + await PacketBuilder.UserPresence(player_spec)) if token.spectating: # remove old spectating, because we found new victim await token.spectating.remove_spectator(token) await player_spec.add_spectator(token) return True
async def main_handler(request: Request): if request.headers.get("user-agent", "") != "osu!": return HTMLResponse(f"<html>{Context.motd_html}</html>") token = request.headers.get("osu-token", None) if token: if token == '': response = await PacketBuilder.UserID(-5) return BanchoResponse(response) # send to re-login token_object = Context.players.get_token(token=token) if not token_object: # send to re-login, because token doesn't exists in storage response = await PacketBuilder.UserID(-5) return BanchoResponse(response) token_object.last_packet_unix = int(time.time()) # packets recieve raw_bytes = KurisoBuffer(None) await raw_bytes.write_to_buffer(await request.body()) response = bytearray() while not raw_bytes.EOF(): packet_id = await raw_bytes.read_u_int_16() _ = await raw_bytes.read_int_8() # empty byte packet_length = await raw_bytes.read_int_32() if packet_id == OsuPacketID.Client_Pong.value: # client just spamming it and tries to say, that he is normal :sip: continue data = await raw_bytes.slice_buffer(packet_length) if token_object.is_restricted and packet_id not in ALLOWED_RESTRICT_PACKETS: logger.wlog( f"[{token_object.token}/{token_object.name}] Ignored packet {packet_id}(account restrict)" ) continue if packet_id in OsuEvent.handlers: # This packet can be handled by OsuEvent Class, call it now! # Oh wait let go this thing in async executor. await OsuEvent.handlers[packet_id](data, token_object) logger.klog( f"[{token_object.token}/{token_object.name}] Has triggered {OsuPacketID(packet_id)} with packet length: {packet_length}" ) else: logger.wlog( f"[Events] Packet ID: {packet_id} not found in events handlers" ) response += token_object.dequeue() response = BanchoResponse(bytes(response), token=token_object.token) return response else: # first login # Structure (new line = "|", already split) # [0] osu! version # [1] plain mac addressed, separated by "." # [2] mac addresses hash set # [3] unique ID # [4] disk ID start_time = time.time() # auth speed benchmark time loginData = (await request.body()).decode().split("\n") if len(loginData) < 3: return BanchoResponse(await PacketBuilder.UserID(-5)) if not await userHelper.check_login(loginData[0], loginData[1], request.client.host): logger.elog( f"[{loginData[0]}] tried to login but failed with password") return BanchoResponse(await PacketBuilder.UserID(-1)) user_data = await userHelper.get_start_user(loginData[0]) if not user_data: return BanchoResponse(await PacketBuilder.UserID(-1)) data = loginData[2].split("|") hashes = data[3].split(":")[:-1] time_offset = int(data[1]) pm_private = data[4] == '1' isTourney = "tourney" in data[0] # check if user already on kuriso if Context.players.get_token(uid=user_data['id']) and not isTourney: # wtf osu await Context.players.get_token(uid=user_data['id']).logout() if (user_data['privileges'] & Privileges.USER_PENDING_VERIFICATION) or \ not await userHelper.user_have_hardware(user_data['id']): # we need to verify our user is_success_verify = await userHelper.activate_user( user_data['id'], user_data['username'], hashes) if not is_success_verify: response = (await PacketBuilder.UserID( -1 ) + await PacketBuilder.Notification( 'Your HWID is not clear. Contact Staff to create account!') ) return BanchoResponse(bytes(response)) else: user_data['privileges'] = KurikkuPrivileges.Normal.value await Context.mysql.execute( "UPDATE hw_user SET activated = 1 WHERE userid = %s AND mac = %s AND unique_id = %s AND disk_id = %s", [user_data['id'], hashes[2], hashes[3], hashes[4]]) if (user_data["privileges"] & KurikkuPrivileges.Normal) != KurikkuPrivileges.Normal and \ (user_data["privileges"] & Privileges.USER_PENDING_VERIFICATION) == 0: logger.elog(f"[{loginData}] Banned chmo tried to login") response = (await PacketBuilder.UserID( -1 ) + await PacketBuilder.Notification( 'You are banned. Join our discord for additional information.') ) return BanchoResponse(bytes(response)) if ((user_data["privileges"] & Privileges.USER_PUBLIC > 0) and (user_data["privileges"] & Privileges.USER_NORMAL == 0)) \ and (user_data["privileges"] & Privileges.USER_PENDING_VERIFICATION) == 0: logger.elog(f"[{loginData}] Locked dude tried to login") response = ( await PacketBuilder.UserID(-1) + await PacketBuilder.Notification( 'You are locked by staff. Join discord and ask for unlock!' )) return BanchoResponse(bytes(response)) if bool(Context.bancho_settings['bancho_maintenance']): # send to user that maintenance if not (user_data['privileges'] & KurikkuPrivileges.Developer): response = (await PacketBuilder.UserID( -1 ) + await PacketBuilder.Notification( 'Kuriso! is in maintenance mode. Please try to login again later.' )) return BanchoResponse(bytes(response)) await Context.mysql.execute( ''' INSERT INTO hw_user (userid, mac, unique_id, disk_id, occurencies) VALUES (%s, %s, %s, %s, 1) ON DUPLICATE KEY UPDATE occurencies = occurencies + 1''', [user_data['id'], hashes[2], hashes[3], hashes[4]] ) # log hardware и не ебёт что osu_version = data[0] await userHelper.setUserLastOsuVer(user_data['id'], osu_version) osuVersionInt = osu_version[1:9] now = datetime.datetime.now() vernow = datetime.datetime(int(osuVersionInt[:4]), int(osuVersionInt[4:6]), int(osuVersionInt[6:8]), 00, 00) deltanow = now - vernow if not osuVersionInt[0].isdigit() or \ deltanow.days > 360 or int(osuVersionInt) < 20200811: response = (await PacketBuilder.UserID( -2 ) + await PacketBuilder.Notification( 'Sorry, you use outdated/bad osu!version. Please update your game to join server' )) return BanchoResponse(bytes(response)) if isTourney: if Context.players.get_token(uid=user_data['id']): # manager was logged before, we need just add additional token token, player = Context.players.get_token( uid=user_data['id']).add_additional_client() else: player = TourneyPlayer( int(user_data['id']), user_data['username'], user_data['privileges'], time_offset, pm_private, 0 if user_data['silence_end'] - int(time.time()) < 0 else user_data['silence_end'] - int(time.time()), is_tourneymode=True, ip=request.client.host) await asyncio.gather(*[ player.parse_friends(), player.update_stats(), player.parse_country(request.client.host) ]) else: # create Player instance finally!!!! player = Player( int(user_data['id']), user_data['username'], user_data['privileges'], time_offset, pm_private, 0 if user_data['silence_end'] - int(time.time()) < 0 else user_data['silence_end'] - int(time.time()), ip=request.client.host) await asyncio.gather(*[ player.parse_friends(), player.update_stats(), player.parse_country(request.client.host) ]) if "ppy.sh" in request.url.netloc and not ( player.is_admin or (player.privileges & KurikkuPrivileges.TournamentStaff == KurikkuPrivileges.TournamentStaff)): return BanchoResponse( bytes(await PacketBuilder.UserID( -5 ) + await PacketBuilder.Notification( 'Sorry, you use outdated connection to server. Please use devserver flag' ))) user_country = await userHelper.get_country(user_data['id']) if user_country == "XX": await userHelper.set_country(user_data['id'], player.country[1]) start_bytes_async = await asyncio.gather(*[ PacketBuilder.UserID(player.id), PacketBuilder.ProtocolVersion(19), PacketBuilder.BanchoPrivileges(player.bancho_privs), PacketBuilder.UserPresence(player), PacketBuilder.UserStats(player), PacketBuilder.FriendList(player.friends), PacketBuilder.SilenceEnd( player.silence_end if player.silence_end > 0 else 0), PacketBuilder.Notification( f'''Welcome to kuriso!\nBuild ver: v{Context.version}\nCommit: {Context.commit_id}''' ), PacketBuilder.Notification( f'Authorization took: {round((time.time() - start_time) * 1000, 4)}ms' ) ]) start_bytes = b''.join(start_bytes_async) if Context.bancho_settings.get('login_notification', None): start_bytes += await PacketBuilder.Notification( Context.bancho_settings.get('login_notification', None)) if Context.bancho_settings.get('bancho_maintenance', None): start_bytes += await PacketBuilder.Notification( 'Don\'t forget enable server after maintenance :sip:') if Context.bancho_settings['menu_icon']: start_bytes += await PacketBuilder.MainMenuIcon( Context.bancho_settings['menu_icon']) if isTourney and Context.players.get_token(uid=user_data['id']): logger.klog( f"[{player.token}/{player.name}] Joined kuriso as additional client for origin!" ) for p in Context.players.get_all_tokens(): if p.is_restricted: continue start_bytes += bytes(await PacketBuilder.UserPresence(p) + await PacketBuilder.UserStats(p)) else: for p in Context.players.get_all_tokens(): if p.is_restricted: continue start_bytes += bytes(await PacketBuilder.UserPresence(p) + await PacketBuilder.UserStats(p)) p.enqueue( bytes(await PacketBuilder.UserPresence(player) + await PacketBuilder.UserStats(player))) await userHelper.saveBanchoSession(player.id, request.client.host) Context.players.add_token(player) await Context.redis.set("ripple:online_users", len(Context.players.get_all_tokens(True))) logger.klog(f"[{player.token}/{player.name}] Joined kuriso!") # default channels to join is #osu, #announce and #english await asyncio.gather(*[ Context.channels['#osu'].join_channel( player), Context.channels['#announce'].join_channel(player), Context.channels['#english'].join_channel(player) ]) for (_, chan) in Context.channels.items(): if not chan.temp_channel and chan.can_read: start_bytes += await PacketBuilder.ChannelAvailable(chan) start_bytes += await PacketBuilder.ChannelListeningEnd() if player.is_restricted: start_bytes += await PacketBuilder.UserRestricted() await CrystalBot.ez_message( player.name, "Your account is currently in restricted mode. Please visit kurikku's website for more information." ) Context.stats['osu_versions'].labels(osu_version=osu_version).inc() Context.stats['devclient_usage'].labels(host=request.url.netloc).inc() return BanchoResponse(start_bytes, player.token)
async def main(): # load dotenv file load_dotenv(find_dotenv()) # Load configuration for our project Config.load_config() logger.slog("[Config] Loaded") # create simple Starlette through uvicorn app app = Starlette(debug=Config.config['debug']) app.add_middleware(ProxyHeadersMiddleware) if Config.config['sentry']['enabled']: sentry_sdk.init(dsn=Config.config['sentry']['url']) app.add_middleware(SentryMiddleware) # load version Context.load_version() logger.klog(f"Hey! Starting kuriso! v{Context.version} (commit-id: {Context.commit_id})") logger.printColored(open("kuriso.MOTD", mode="r", encoding="utf-8").read(), logger.YELLOW) # Load all events & handlers registrator.load_handlers(app) # Create Redis connection :sip: logger.wlog("[Redis] Trying connection to Redis") redis_values = dict( db=Config.config['redis']['db'], minsize=5, maxsize=10 ) if Config.config['redis']['password']: redis_values['password'] = Config.config['redis']['password'] redis_pool = await aioredis.create_redis_pool( f"redis://{Config.config['redis']['host']}", **redis_values ) Context.redis = redis_pool logger.slog("[Redis] Connection to Redis established! Well done!") logger.slog("[Redis] Removing old information about redis...") try: await Context.redis.set("ripple:online_users", "0") redis_flush_script = ''' local matches = redis.call('KEYS', ARGV[1]) local result = 0 for _,key in ipairs(matches) do result = result + redis.call('DEL', key) end return result ''' await Context.redis.eval(redis_flush_script, args=["peppy:*"]) await Context.redis.eval(redis_flush_script, args=["peppy:sessions:*"]) except Exception as e: traceback.print_exc() capture_exception(e) logger.elog("[Redis] initiation data ruined... Check this!") await Context.redis.set("peppy:version", Context.version) logger.wlog("[MySQL] Making connection to MySQL Database...") mysql_pool = AsyncSQLPoolWrapper() await mysql_pool.connect(**{ 'host': Config.config['mysql']['host'], 'user': Config.config['mysql']['user'], 'password': Config.config['mysql']['password'], 'port': Config.config['mysql']['port'], 'db': Config.config['mysql']['database'], 'loop': asyncio.get_event_loop(), 'autocommit': True }) Context.mysql = mysql_pool logger.slog("[MySQL] Connection established!") if Config.config['prometheus']['enabled']: logger.wlog("[Prometheus stats] Loading...") prometheus_client.start_http_server( Config.config['prometheus']['port'], addr=Config.config['prometheus']['host'] ) logger.slog("[Prometheus stats] Metrics started...") # now load bancho settings await Context.load_bancho_settings() await registrator.load_default_channels() from bot.bot import CrystalBot # now load bot await CrystalBot.connect() # and register bot commands CrystalBot.load_commands() logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) scheduler = AsyncIOScheduler() scheduler.start() scheduler.add_job(loops.clean_timeouts, "interval", seconds=60) if Config.config['prometheus']['enabled']: scheduler.add_job(loops.add_prometheus_stats, "interval", seconds=15) scheduler.add_job(loops.add_stats, "interval", seconds=120) # Setup pub/sub listeners for LETS/old admin panel events event_loop = asyncio.get_event_loop() event_loop.create_task(pubsub_listeners.init()) Context.load_motd() uvicorn.run(app, host=Config.config['host']['address'], port=Config.config['host']['port'], access_log=False)
async def _shutdown(): logger.elog("[System] Disposing server!") logger.elog("[System] Disposing players!") for player in Context.players.get_all_tokens(): await player.say_bancho_restarting() logger.elog("[Server] Awaiting when players will get them packets!") attempts = 0 while any(len(x.queue) > 0 for x in Context.players.get_all_tokens()): await asyncio.sleep(5) attempts += 1 logger.elog(f"[Server] Attempt {attempts}/3") if attempts == 3: break # Stop redis connection logger.elog("[Server] Stopping redis pool...") if Context.redis: Context.redis.close() await Context.redis.wait_closed() # Stop redis sub connection logger.elog("[Server] Stopping redis subscriber pool...") if Context.redis_sub: Context.redis_sub.close() await Context.redis_sub.wait_closed() # Stop mysql pool connection logger.elog("[Server] Stopping mysql pool...") if Context.mysql: Context.mysql.pool.close() await Context.mysql.pool.wait_closed() logger.elog("[Server] Disposing uvicorn instance...")
async def proceed_command(cls, message: 'Message') -> Union[bool]: if message.sender == cls.bot_name: return False sender = Context.players.get_token(uid=message.client_id) if not sender: return False if message.to.startswith("#multi"): # convert it into normal if sender.match: message.to = f"#multi_{sender.match.id}" if message.to.startswith("#spec"): if sender.spectating: message.to = f"#spec_{sender.spectating.id}" message.body = message.body.strip() cmd, func_command = None, None for (k, func) in cls.commands.items(): if message.body.startswith(k): cmd, func_command = k, func break if not cmd: return False comand = cmd args = shlex.split(message.body[len(cmd):].replace("'", "\\'").replace( '"', '\\"'), posix=True) cdUser = cls.cd.get(sender.id, None) nowTime = int(time.time()) if cdUser: if nowTime - cdUser <= cls.cool_down: # Checking users cooldown cls.cd[sender.id] = nowTime return False cls.cd[sender.id] = nowTime else: # If user not write something after bot running cls.cd[sender.id] = nowTime result = None try: result = await func_command(args, sender, message) except Exception as e: logger.elog(f"[Bot] {sender.name} with {comand} crashed {args}") capture_exception(e) traceback.print_exc() return await cls.token.send_message( Message(sender=cls.token.name, body='Command crashed, write to KotRik!!!', to=message.sender, client_id=cls.token.id)) if result: await cls.token.send_message( Message(sender=cls.token.name, body=result, to=message.to if message.to.startswith("#") else message.sender, client_id=cls.token.id)) return True