Exemple #1
0
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.'
Exemple #2
0
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)
Exemple #3
0
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!
        )
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
    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.')
Exemple #7
0
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
Exemple #8
0
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''
Exemple #9
0
    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)
Exemple #10
0
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)
Exemple #11
0
    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)
Exemple #12
0
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!
Exemple #13
0
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)
Exemple #14
0
        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:
Exemple #15
0
    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)
Exemple #16
0
# 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'
Exemple #17
0
    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')
Exemple #18
0
    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()
Exemple #19
0
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.
Exemple #20
0
 async def callback():
     # this is called when the menu item is clicked
     p.enqueue(packets.notification('clicked!'))
Exemple #21
0
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
Exemple #22
0
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)
Exemple #23
0
    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')
Exemple #24
0
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
Exemple #25
0
        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>'
Exemple #26
0
    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
Exemple #27
0
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.'