async def refresh_user_stats(packet_data: bytes, token: 'Player'): if not token.is_tourneymode: return False # not allow use that packet for non-tourney player # it's good right, but i want to fix issue that i described in TourneyPlayer.py description # this packet can be send only by manager, if this packet was received by player without attr additional_clients # in 99% it was issue that i described :D # in that case we should firstly switch our Player objects and than proceed request if not hasattr(token, "additional_clients"): logger.wlog("[Events] Was found bad tourney clients order. Fixing it!") old_token = token.token manager_obj = Context.players.get_token(uid=token.id) manager_token = manager_obj.token manager_obj.additional_clients.pop(old_token) # remove our actual manager form additional clients token.token = manager_token # moving manager token to additional token manager_obj.additional_clients[manager_token] = token # add this token to additional clients Context.players.store_by_token.pop(manager_token) # remove our additional client from manager accounts manager_obj.token = old_token # assign our pseudo additional client to manager Context.players.store_by_token[old_token] = manager_obj # store this token, like it should be token = manager_obj # for next code part match_id = await PacketResolver.read_match_id(packet_data) if match_id not in Context.matches: return False token.enqueue(await PacketBuilder.UpdateMatch(Context.matches.get(match_id), False)) return True
async def kick( self, message: str = "You have been kicked from the server. Please login again.", reason: str = "kick") -> bool: if self.is_bot: return False logger.wlog(f"[Player/{self.name}] has been disconnected. {reason}") if message: self.enqueue(await PacketBuilder.Notification(message)) self.enqueue(await PacketBuilder.UserID(-1)) # login failed await self.logout() return True
def load_handlers(app: Starlette): logger.wlog("[Handlers/Events] Loading handlers & events...") paths_to_import = {'handlers': ['httphandlers', 'eventhandlers']} for (k, v) in paths_to_import.items(): sys.path.insert(0, k) for deep_path in v: sys.path.insert(0, f'{k}/{deep_path}') folder_files = os.listdir(f"{k}/{deep_path}") for file in folder_files: if file.endswith(".py"): logger.slog(f"[Handlers/Events] file {file} loaded! ") sys.path.insert(0, f"{k}/{deep_path}/{file}") __import__(os.path.splitext(file)[0], None, None, ['']) handlers = [] for (path, path_describe) in HttpEvent.handlers.items(): logger.slog(f"[Handlers/Events] {path} registered!") handlers.append(Route(path, endpoint=path_describe['func'], methods=path_describe['methods'])) app.mount('', Router(handlers)) 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)