Browse Source

Merge remote-tracking branch 'github/master'

pull/8/head 2.0.2
Richard Neumann 3 years ago
parent
commit
dd8dde8dd0
  1. 5
      rcon/__init__.py
  2. 7
      rcon/battleye/__init__.py
  3. 11
      rcon/battleye/client.py
  4. 9
      rcon/battleye/proto.py
  5. 4
      rcon/client.py
  6. 2
      rcon/config.py
  7. 20
      rcon/console.py
  8. 10
      rcon/errorhandler.py
  9. 24
      rcon/exceptions.py
  10. 43
      rcon/gui.py
  11. 13
      rcon/rconclt.py
  12. 14
      rcon/rconshell.py
  13. 3
      rcon/source/__init__.py
  14. 4
      rcon/source/async_rcon.py
  15. 4
      rcon/source/client.py
  16. 19
      rcon/source/exceptions.py
  17. 6
      setup.py

5
rcon/__init__.py

@ -1,6 +1 @@
"""RCON client library."""
from rcon.source import RequestIdMismatch, WrongPassword, Client, rcon
__all__ = ['RequestIdMismatch', 'WrongPassword', 'Client', 'rcon']

7
rcon/battleye/__init__.py

@ -1 +1,6 @@
"""BattlEye RCON implementation."""
"""BattlEye RCON implementation."""
from rcon.battleye.client import Client
__all__ = ['Client']

11
rcon/battleye/client.py

@ -17,15 +17,18 @@ Host = Union[str, IPv4Address]
class Client(BaseClient, socket_type=SOCK_DGRAM):
"""BattlEye RCon client."""
def communicate(self, data: bytes, *, recv: int = 4096) -> bytes:
def communicate(self, data: bytes, *, size: int = 4096) -> bytes:
"""Sends and receives packets."""
self._socket.send(data)
return self._socket.recv(recv)
return self._socket.recv(size)
def login(self, passwd: str) -> bytes:
"""Logs the user in."""
return self.communicate(bytes(LoginRequest.from_passwd(passwd)))
def command(self, command: str) -> bytes:
def run(self, command: str, *args: str) -> str:
"""Executes a command."""
return self.communicate(bytes(Command.from_command(command)))
packet = Command.from_command(command, *args)
_ = self.communicate(bytes(packet))
# TODO: Process response
return ''

9
rcon/battleye/proto.py

@ -99,6 +99,11 @@ class Command(NamedTuple):
return Header.from_payload(self.payload)
@classmethod
def from_command(cls, command: str):
"""Creates a command packet from the given command."""
def from_string(cls, command: str):
"""Creates a command packet from the given string."""
return cls(0x01, 0x00, command)
@classmethod
def from_command(cls, command: str, *args: str):
"""Creates a command packet from the command and arguments."""
return cls.from_string(' '.join([command, *args]))

4
rcon/client.py

@ -63,3 +63,7 @@ class BaseClient:
def login(self, passwd: str) -> bool:
"""Performs a login."""
raise NotImplementedError()
def run(self, command: str, *args: str) -> str:
"""Runs a command."""
raise NotImplementedError()

2
rcon/source/config.py → rcon/config.py

@ -9,7 +9,7 @@ from os import getenv, name
from pathlib import Path
from typing import Iterable, NamedTuple, Optional, Union
from rcon.source.exceptions import ConfigReadError, UserAbort
from rcon.exceptions import ConfigReadError, UserAbort
__all__ = ['CONFIG_FILES', 'LOG_FORMAT', 'SERVERS', 'Config', 'from_args']

20
rcon/source/console.py → rcon/console.py

@ -1,10 +1,11 @@
"""An interactive console."""
from getpass import getpass
from typing import Type
from rcon.source.client import Client
from rcon.source.config import Config
from rcon.source.exceptions import RequestIdMismatch, WrongPassword
from rcon.client import BaseClient
from rcon.config import Config
from rcon.exceptions import SessionTimeout, WrongPassword
__all__ = ['PROMPT', 'rconcmd']
@ -75,7 +76,7 @@ def get_config(host: str, port: int, passwd: str) -> Config:
return Config(host, port, passwd)
def login(client: Client, passwd: str) -> str:
def login(client: BaseClient, passwd: str) -> str:
"""Performs a login."""
while True:
@ -89,7 +90,7 @@ def login(client: Client, passwd: str) -> str:
return passwd
def process_input(client: Client, passwd: str, prompt: str) -> bool:
def process_input(client: BaseClient, passwd: str, prompt: str) -> bool:
"""Processes the CLI input."""
try:
@ -111,7 +112,7 @@ def process_input(client: Client, passwd: str, prompt: str) -> bool:
try:
result = client.run(command, *args)
except RequestIdMismatch:
except SessionTimeout:
print(MSG_SESSION_TIMEOUT)
try:
@ -125,7 +126,10 @@ def process_input(client: Client, passwd: str, prompt: str) -> bool:
return True
def rconcmd(host: str, port: int, passwd: str, *, prompt: str = PROMPT):
def rconcmd(
client_cls: Type[BaseClient], host: str, port: int, passwd: str, *,
prompt: str = PROMPT
):
"""Initializes the console."""
try:
@ -136,7 +140,7 @@ def rconcmd(host: str, port: int, passwd: str, *, prompt: str = PROMPT):
prompt = prompt.format(host=host, port=port)
with Client(host, port) as client:
with client_cls(host, port) as client:
try:
passwd = login(client, passwd)
except EOFError:

10
rcon/source/errorhandler.py → rcon/errorhandler.py

@ -3,10 +3,10 @@
from logging import Logger
from socket import timeout
from rcon.source.exceptions import ConfigReadError
from rcon.source.exceptions import RequestIdMismatch
from rcon.source.exceptions import UserAbort
from rcon.source.exceptions import WrongPassword
from rcon.exceptions import ConfigReadError
from rcon.exceptions import SessionTimeout
from rcon.exceptions import UserAbort
from rcon.exceptions import WrongPassword
__all__ = ['ErrorHandler']
@ -18,7 +18,7 @@ ERRORS = {
ConnectionRefusedError: (3, 'Connection refused.'),
(TimeoutError, timeout): (4, 'Connection timed out.'),
WrongPassword: (5, 'Wrong password.'),
RequestIdMismatch: (6, 'Session timed out.')
SessionTimeout: (6, 'Session timed out.')
}

24
rcon/exceptions.py

@ -0,0 +1,24 @@
"""Common exceptions."""
__all__ = [
'ConfigReadError',
'SessionTimeout',
'UserAbort',
'WrongPassword'
]
class ConfigReadError(Exception):
"""Indicates an error while reading the configuration."""
class SessionTimeout(Exception):
"""Indicates that the session timed out."""
class UserAbort(Exception):
"""Indicates that a required action has been aborted by the user."""
class WrongPassword(Exception):
"""Indicates a wrong password."""

43
rcon/source/gui.py → rcon/gui.py

@ -6,15 +6,16 @@ from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv, name
from pathlib import Path
from socket import gaierror, timeout
from typing import Iterable, NamedTuple
from typing import Iterable, NamedTuple, Type
from gi import require_version
require_version('Gtk', '3.0')
from gi.repository import Gtk
from rcon.source.client import Client
from rcon.source.config import LOG_FORMAT
from rcon.source.exceptions import RequestIdMismatch, WrongPassword
from rcon import battleye, source
from rcon.client import BaseClient
from rcon.config import LOG_FORMAT
from rcon.exceptions import SessionTimeout, WrongPassword
__all__ = ['main']
@ -36,10 +37,18 @@ def get_args() -> Namespace:
"""Parses the command line arguments."""
parser = ArgumentParser(description='A minimalistic, GTK-based RCON GUI.')
parser.add_argument('-d', '--debug', action='store_true',
help='print additional debug information')
parser.add_argument('-t', '--timeout', type=float, metavar='seconds',
help='connection timeout in seconds')
parser.add_argument(
'-B', '--battleye', action='store_true',
help='use BattlEye RCon instead of Source RCON'
)
parser.add_argument(
'-d', '--debug', action='store_true',
help='print additional debug information'
)
parser.add_argument(
'-t', '--timeout', type=float, metavar='seconds',
help='connection timeout in seconds'
)
return parser.parse_args()
@ -96,6 +105,11 @@ class GUI(Gtk.Window): # pylint: disable=R0902
self.load_gui_settings()
@property
def client_cls(self) -> Type[BaseClient]:
"""Returns the client class."""
return battleye.Client if self.args.battleye else source.Client
@property
def result_text(self) -> str:
"""Returns the result text."""
@ -141,7 +155,7 @@ class GUI(Gtk.Window): # pylint: disable=R0902
def load_gui_settings(self) -> None:
"""Loads the GUI settings from the cache file."""
try:
with CACHE_FILE.open('r') as cache:
with CACHE_FILE.open('rb') as cache:
self.gui_settings = load(cache)
except FileNotFoundError:
LOGGER.warning('Cache file not found: %s', CACHE_FILE)
@ -153,7 +167,7 @@ class GUI(Gtk.Window): # pylint: disable=R0902
def save_gui_settings(self):
"""Saves the GUI settings to the cache file."""
try:
with CACHE_FILE.open('w') as cache:
with CACHE_FILE.open('wb') as cache:
dump(self.gui_settings, cache)
except PermissionError:
LOGGER.error('Insufficient permissions to read: %s', CACHE_FILE)
@ -171,7 +185,7 @@ class GUI(Gtk.Window): # pylint: disable=R0902
def run_rcon(self) -> str:
"""Returns the current RCON settings."""
with Client(
with self.client_cls(
self.host.get_text().strip(),
self.port.get_value_as_int(),
timeout=self.args.timeout,
@ -193,11 +207,8 @@ class GUI(Gtk.Window): # pylint: disable=R0902
self.show_error('Connection timed out.')
except WrongPassword:
self.show_error('Wrong password.')
except RequestIdMismatch as mismatch:
self.show_error(
'Request ID mismatch.\n'
f'Expected {mismatch.sent}, but got {mismatch.received}.'
)
except SessionTimeout:
self.show_error('Session timed out.')
else:
self.result_text = result

13
rcon/source/rconclt.py → rcon/rconclt.py

@ -4,9 +4,9 @@ from argparse import ArgumentParser, Namespace
from logging import DEBUG, INFO, basicConfig, getLogger
from pathlib import Path
from rcon.source.client import Client
from rcon.source.config import CONFIG_FILES, LOG_FORMAT, from_args
from rcon.source.errorhandler import ErrorHandler
from rcon import battleye, source
from rcon.config import CONFIG_FILES, LOG_FORMAT, from_args
from rcon.errorhandler import ErrorHandler
__all__ = ['main']
@ -20,6 +20,10 @@ def get_args() -> Namespace:
parser = ArgumentParser(description='A Minecraft RCON client.')
parser.add_argument('server', help='the server to connect to')
parser.add_argument(
'-B', '--battleye', action='store_true',
help='use BattlEye RCon instead of Source RCON'
)
parser.add_argument(
'-c', '--config', type=Path, metavar='file', default=CONFIG_FILES,
help='the configuration file'
@ -45,8 +49,9 @@ def run() -> None:
args = get_args()
basicConfig(format=LOG_FORMAT, level=DEBUG if args.debug else INFO)
host, port, passwd = from_args(args)
client_cls = battleye.Client if args.battleye else source.Client
with Client(host, port, timeout=args.timeout) as client:
with client_cls(host, port, timeout=args.timeout) as client:
client.login(passwd)
text = client.run(args.command, *args.argument)

14
rcon/source/rconshell.py → rcon/rconshell.py

@ -4,10 +4,11 @@ from argparse import ArgumentParser, Namespace
from logging import INFO, basicConfig, getLogger
from pathlib import Path
from rcon import battleye, source
from rcon.readline import CommandHistory
from rcon.source.config import CONFIG_FILES, LOG_FORMAT, from_args
from rcon.source.console import PROMPT, rconcmd
from rcon.source.errorhandler import ErrorHandler
from rcon.config import CONFIG_FILES, LOG_FORMAT, from_args
from rcon.console import PROMPT, rconcmd
from rcon.errorhandler import ErrorHandler
__all__ = ['get_args', 'main']
@ -21,6 +22,10 @@ def get_args() -> Namespace:
parser = ArgumentParser(description='An interactive RCON shell.')
parser.add_argument('server', nargs='?', help='the server to connect to')
parser.add_argument(
'-B', '--battleye', action='store_true',
help='use BattlEye RCon instead of Source RCON'
)
parser.add_argument(
'-c', '--config', type=Path, metavar='file', default=CONFIG_FILES,
help='the configuration file'
@ -37,6 +42,7 @@ def run() -> None:
args = get_args()
basicConfig(level=INFO, format=LOG_FORMAT)
client_cls = battleye.Client if args.battleye else source.Client
if args.server:
host, port, passwd = from_args(args)
@ -44,7 +50,7 @@ def run() -> None:
host = port = passwd = None
with CommandHistory(LOGGER):
rconcmd(host, port, passwd, prompt=args.prompt)
rconcmd(client_cls, host, port, passwd, prompt=args.prompt)
def main() -> int:

3
rcon/source/__init__.py

@ -2,7 +2,6 @@
from rcon.source.async_rcon import rcon
from rcon.source.client import Client
from rcon.source.exceptions import RequestIdMismatch, WrongPassword
__all__ = ['RequestIdMismatch', 'WrongPassword', 'Client', 'rcon']
__all__ = ['Client', 'rcon']

4
rcon/source/async_rcon.py

@ -2,7 +2,7 @@
from asyncio import StreamReader, StreamWriter, open_connection
from rcon.source.exceptions import RequestIdMismatch, WrongPassword
from rcon.exceptions import SessionTimeout, WrongPassword
from rcon.source.proto import Packet, Type
@ -56,6 +56,6 @@ async def rcon(
await close(writer)
if response.id != request.id:
raise RequestIdMismatch(request.id, response.id)
raise SessionTimeout()
return response.payload.decode(encoding)

4
rcon/source/client.py

@ -1,7 +1,7 @@
"""Synchronous client."""
from rcon.client import BaseClient
from rcon.source.exceptions import RequestIdMismatch, WrongPassword
from rcon.exceptions import SessionTimeout, WrongPassword
from rcon.source.proto import Packet, Type
@ -44,6 +44,6 @@ class Client(BaseClient):
response = self.communicate(request)
if response.id != request.id:
raise RequestIdMismatch(request.id, response.id)
raise SessionTimeout()
return response.payload.decode(encoding)

19
rcon/source/exceptions.py

@ -1,15 +1,6 @@
"""RCON exceptions."""
__all__ = [
'ConfigReadError',
'RequestIdMismatch',
'UserAbort',
'WrongPassword'
]
class ConfigReadError(Exception):
"""Indicates an error while reading the configuration."""
__all__ = ['RequestIdMismatch']
class RequestIdMismatch(Exception):
@ -20,11 +11,3 @@ class RequestIdMismatch(Exception):
super().__init__()
self.sent = sent
self.received = received
class UserAbort(Exception):
"""Indicates that a required action has been aborted by the user."""
class WrongPassword(Exception):
"""Indicates a wrong password."""

6
setup.py

@ -18,9 +18,9 @@ setup(
extras_require={'GUI': ['pygobject', 'pygtk']},
entry_points={
'console_scripts': [
'rcongui = rcon.source.gui:main',
'rconclt = rcon.source.rconclt:main',
'rconshell = rcon.source.rconshell:main',
'rcongui = rcon.gui:main',
'rconclt = rcon.rconclt:main',
'rconshell = rcon.rconshell:main',
],
},
url='https://github.com/conqp/rcon',

Loading…
Cancel
Save