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
Пример #2
0
    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
Пример #3
0
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
Пример #4
0
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)
Пример #5
0
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)