Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-ascii player name causes RCON showplayers break #4

Closed
sh-cho opened this issue Feb 4, 2024 · 7 comments
Closed

Non-ascii player name causes RCON showplayers break #4

sh-cho opened this issue Feb 4, 2024 · 7 comments

Comments

@sh-cho
Copy link
Contributor

sh-cho commented Feb 4, 2024

Problem

In Source RCON protocol, packet body is defined as Null-terminated ASCII String. But we can set palworld player name as non-ascii characters(ex. Korean, Chinese, Cyrillic, ...) and this sets showplayers response packet body as non-ascii character, finally RCON client is failed or stuck.

Similar issues:

I think this should be mainly handled by RCON client.

But it looks like exporter's polling is somewhat problematic.

Screenshot 2024-02-04 at 9 13 18 PM

This is timeline of my server when non-ascii name user joined. After user joined, somehow player count is keep exported(which is outdated) and players info is not exported. (2~3 persons playing in the server at the time)

Suggestion

  • disable polling (or add on/off option)
    • I believe this would be more appropriate to control this in prometheus. So every attempt to get metric => generate current snapshot.
  • replace or modify RCON client package
    • For now, I haven't tested which python RCON package can handle this situation 🥲
@bostrt
Copy link
Contributor

bostrt commented Feb 4, 2024

Thanks for this report, this is great. I really want to disable polling and will look into that.

As for testing with non-ascii I will have to look into that some more. I'm not sure about support by various libraries either.

@bostrt
Copy link
Contributor

bostrt commented Feb 4, 2024

It looks like non-ascii character support is a problem to be resolved in Palworld itself. gorcon/rcon-cli#35

I will continue looking for workarounds in the meantime.

Do you know of any other tools/libraries (non-python) that work fine?

@bostrt
Copy link
Contributor

bostrt commented Feb 4, 2024

I'm leaving this error here for reference. When a Player has a name using non-ascii, it results in this error:

2024-02-04:15:41:40.936 ERROR    [exporter.py:97] Received few bytes!

The best I can tell is, Palworld's RCON implementation is truncating the player names or ends of the string for some reason when they have non-ascii characters which results in the "Received [too] few bytes". The size of the ShowPlayers response must be calculated before they are sent across the wire.

@sh-cho
Copy link
Contributor Author

sh-cho commented Feb 5, 2024

Thanks for the check! I haven't tested any other rcon library, but even though some library can handle non-ascii chars, player id will be trimmed, so.. looks like it's best to wait palworld patch ...

@sh-cho sh-cho closed this as completed Feb 5, 2024
@bostrt
Copy link
Contributor

bostrt commented Feb 5, 2024

I still plan on disabling polling as requested in this issue. Should have that ready today or tomorrow.

@bostrt
Copy link
Contributor

bostrt commented Feb 6, 2024

Polling/caching is no longer used as of v1.1.0 https://github.com/palworldlol/palworld-exporter/releases/tag/v1.1.0

@AWL-Gaming
Copy link

Even using this modified rcon.py code to attempt a base64 decoding fails:

import base64
import logging
import csv
import re
from abc import ABC, abstractmethod
from typing import ContextManager, Generic, List, TypeVar
from rcon import Console
from palworld_exporter.providers.data import Player, ServerInfo


class RCONContext(ContextManager):
    """
    A context-aware class that returns an instance of a RCON Console.
    """

    def __init__(self, host, port, password, timeout=20):
        self._host = host
        self._port = port
        self._password = password
        self._timeout = timeout
        self._first_connection_made = False

    def _get_console(self):
        return Console(self._host, self._password, self._port, self._timeout)

    def _check_encoding(self, response):
        """Check if the response is base64-encoded and return a boolean."""
        try:
            decoded_response = base64.b64decode(response)
            return base64.b64encode(decoded_response).decode("utf-8") == response
        except Exception:
            return False

    def _decode_if_needed(self, response):
        """Decode the response if it was base64 encoded."""
        if self._check_encoding(response):
            return base64.b64decode(response).decode("utf-8")
        return response

    def __enter__(self) -> Console:
        self._console = self._get_console()
        if not self._first_connection_made:
            logging.info('RCON Connection success')
            self._first_connection_made = True
        logging.debug('RCON collector opened connection')
        return self._console

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if self._console:
            self._console.close()
            logging.debug('RCON collector closed connection')


T = TypeVar('T')

class RCONProvider(ABC, Generic[T]):
    @abstractmethod
    def fetch(self) -> T:
        raise NotImplementedError


class PlayersProvider(RCONProvider[List[Player]]):
    """
    Get active Player information.
    """

    def __init__(self, rcon_ctx: RCONContext, ignore_logging_in: bool):
        self._rcon_ctx = rcon_ctx
        self._ignore_logging_in = ignore_logging_in

    def _cmd_showplayers(self):
        with self._rcon_ctx as conn:
            try:
                result = conn.command("ShowPlayers")
                logging.debug(f"Raw ShowPlayers response: {result}")

                if not result or len(result.strip()) < 10:
                    raise Exception("Received invalid or empty response from ShowPlayers.")
                
                # Attempt UTF-8 decode and fall back to other encodings if necessary
                result = self._decode_if_needed(result)
                return result
            except Exception as e:
                logging.error(f"Error while fetching ShowPlayers: {e}")
                return ""  # Return empty string in case of error

    def _decode_if_needed(self, result: bytes):
        try:
            # Attempt UTF-8 decode
            return result.decode('utf-8', errors='replace')
        except UnicodeDecodeError:
            logging.warning("UTF-8 decoding failed, attempting Latin-1 decoding.")
            # Fallback to Latin-1 or Windows-1252 encoding if UTF-8 fails
            return result.decode('latin-1', errors='replace')

    def fetch(self) -> List[Player]:
        player_resp = self._cmd_showplayers()
        if player_resp:
            player_reader = csv.reader(player_resp.split('\n'))
            fields = next(player_reader)
            if fields:
                players = []
                for row in player_reader:
                    if row:
                        if row[1] == '00000000' and self._ignore_logging_in:
                            logging.debug(f'Excluding player {row[0]}')
                            continue
                        players.append(Player(row[0], row[1], row[2]))
                return players
            else:
                logging.warning('Empty field list from ShowPlayers')
                return []
        else:
            logging.warning('Empty or null response from ShowPlayers')
            return []


class ServerInfoProvider(RCONProvider[ServerInfo]):
    """
    Get Server information including name and version.
    """

    def __init__(self, rcon_ctx: RCONContext):
        self._rcon_ctx = rcon_ctx
        self._info_re = re.compile(r"\[v(?P<version>.*?)\] (?P<name>.*$)")

    def _cmd_info(self):
        with self._rcon_ctx as conn:
            result = conn.command("Info")
            # Decode if base64-encoded
            return self._rcon_ctx._decode_if_needed(result)

    def fetch(self) -> ServerInfo:
        info_resp = self._cmd_info()
        info = self._info_re.search(info_resp)
        if info:
            version = info.group("version")
            name = info.group("name").strip()
            return ServerInfo(name, version)
        else:
            logging.warn('No response from Info RCON command')
            return ServerInfo('unknown', 'unknown')
2024-12-26:12:29:36.280 WARNING  [rcon.py:114] Empty or null response from ShowPlayers
2024-12-26:12:29:51.383 ERROR    [rcon.py:84] Error while fetching ShowPlayers: Received few bytes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants