rconx 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. rconx/__init__.py +68 -0
  2. rconx/__main__.py +5 -0
  3. rconx/cli/__init__.py +0 -0
  4. rconx/cli/arguments.py +128 -0
  5. rconx/cli/main.py +78 -0
  6. rconx/client/__init__.py +0 -0
  7. rconx/client/auth/__init__.py +1 -0
  8. rconx/client/auth/base.py +13 -0
  9. rconx/client/auth/source_handler.py +69 -0
  10. rconx/client/client.py +463 -0
  11. rconx/client/client_options.py +17 -0
  12. rconx/client/command/__init__.py +1 -0
  13. rconx/client/command/base.py +13 -0
  14. rconx/client/command/common.py +11 -0
  15. rconx/client/command/cuberite_handler.py +70 -0
  16. rconx/client/command/idle_timeout_handler.py +50 -0
  17. rconx/client/command/minecraft_handler.py +65 -0
  18. rconx/client/command/single_packet_handler.py +35 -0
  19. rconx/client/command/srcds_handler.py +80 -0
  20. rconx/client/operation.py +82 -0
  21. rconx/client/preset.py +22 -0
  22. rconx/client/raw.py +235 -0
  23. rconx/common/__init__.py +0 -0
  24. rconx/common/async_utils.py +26 -0
  25. rconx/common/connection.py +412 -0
  26. rconx/common/connection_info.py +21 -0
  27. rconx/common/connection_options.py +15 -0
  28. rconx/common/content.py +28 -0
  29. rconx/common/exceptions.py +26 -0
  30. rconx/common/protocol.py +66 -0
  31. rconx/common/thread_pool.py +162 -0
  32. rconx/common/utils.py +63 -0
  33. rconx/py.typed +0 -0
  34. rconx/server/__init__.py +0 -0
  35. rconx/server/config.py +34 -0
  36. rconx/server/log.py +5 -0
  37. rconx/server/raw.py +301 -0
  38. rconx/server/raw_options.py +12 -0
  39. rconx/server/server.py +233 -0
  40. rconx/server/server_connection.py +75 -0
  41. rconx/server/server_options.py +35 -0
  42. rconx/server/session.py +173 -0
  43. rconx-0.1.0.dist-info/METADATA +93 -0
  44. rconx-0.1.0.dist-info/RECORD +46 -0
  45. rconx-0.1.0.dist-info/WHEEL +4 -0
  46. rconx-0.1.0.dist-info/licenses/LICENSE +165 -0
rconx/__init__.py ADDED
@@ -0,0 +1,68 @@
1
+ from rconx.client.client import AsyncRconClient, RconClient
2
+ from rconx.client.client_options import RconClientOptions
3
+ from rconx.client.preset import RconPreset
4
+ from rconx.client.raw import AsyncRawRconClient, LocalAddress, RawRconClient
5
+ from rconx.common.connection import AsyncConnection, Connection
6
+ from rconx.common.connection_info import ConnectionAddress, ConnectionInfo
7
+ from rconx.common.connection_options import ConnectionOptions
8
+ from rconx.common.content import RconContent
9
+ from rconx.common.exceptions import (
10
+ RconAuthenticationError,
11
+ RconConnectionError,
12
+ RconError,
13
+ RconPacketDecodeError,
14
+ RconProtocolError,
15
+ RconStateError,
16
+ RconTimeoutError,
17
+ )
18
+ from rconx.common.protocol import Packet, PacketType
19
+ from rconx.server.raw import AsyncRawRconServer, RawRconServer
20
+ from rconx.server.raw_options import RawRconServerOptions
21
+ from rconx.server.server import AsyncRconServer, RconServer
22
+ from rconx.server.server_options import RconServerOptions
23
+
24
+ __all__ = [
25
+ # High-level clients
26
+ 'RconClient',
27
+ 'AsyncRconClient',
28
+ 'RconPreset',
29
+ 'RconClientOptions',
30
+
31
+ # Low-level clients
32
+ 'RawRconClient',
33
+ 'AsyncRawRconClient',
34
+ 'LocalAddress',
35
+
36
+ # High-level servers
37
+ 'RconServer',
38
+ 'AsyncRconServer',
39
+ 'RconServerOptions',
40
+
41
+ # Low-level servers
42
+ 'RawRconServer',
43
+ 'AsyncRawRconServer',
44
+ 'RawRconServerOptions',
45
+
46
+ # Low-level connections
47
+ 'Connection',
48
+ 'AsyncConnection',
49
+ 'ConnectionOptions',
50
+
51
+ # Shared data
52
+ 'RconContent',
53
+ 'ConnectionInfo',
54
+ 'ConnectionAddress',
55
+
56
+ # Protocol
57
+ 'Packet',
58
+ 'PacketType',
59
+
60
+ # Exceptions
61
+ 'RconError',
62
+ 'RconConnectionError',
63
+ 'RconTimeoutError',
64
+ 'RconPacketDecodeError',
65
+ 'RconAuthenticationError',
66
+ 'RconStateError',
67
+ 'RconProtocolError',
68
+ ]
rconx/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from rconx.cli.main import main
2
+
3
+
4
+ if __name__ == '__main__':
5
+ main()
rconx/cli/__init__.py ADDED
File without changes
rconx/cli/arguments.py ADDED
@@ -0,0 +1,128 @@
1
+ import argparse
2
+ import dataclasses
3
+ import os
4
+ import sys
5
+ from typing import List, Optional
6
+
7
+ from rconx.client.preset import RconPreset
8
+ from rconx.common.connection_info import ConnectionAddress
9
+ from rconx.common.utils import validate_packet_size_limit, validate_timeout
10
+
11
+
12
+ def __get_self_version() -> str:
13
+ try:
14
+ if sys.version_info >= (3, 8):
15
+ from importlib.metadata import version
16
+ else:
17
+ from importlib_metadata import version
18
+ return version('rconx')
19
+ except Exception:
20
+ return 'unknown'
21
+
22
+
23
+ @dataclasses.dataclass(frozen=True)
24
+ class SizeLimit:
25
+ value: Optional[int]
26
+
27
+
28
+ @dataclasses.dataclass(frozen=True)
29
+ class CliOptions:
30
+ address: ConnectionAddress
31
+ password: str = dataclasses.field(repr=False)
32
+ commands: List[str]
33
+ preset: RconPreset
34
+ interactive: bool
35
+ timeout: Optional[float]
36
+ idle_window: Optional[float]
37
+ encoding: Optional[str]
38
+ raw: bool
39
+ max_send_packet_size: Optional[SizeLimit]
40
+ max_receive_packet_size: Optional[SizeLimit]
41
+ max_response_size: Optional[SizeLimit]
42
+
43
+
44
+ def compute_address(value: str) -> ConnectionAddress:
45
+ if value.startswith('['):
46
+ closing = value.find(']')
47
+ if closing < 0 or value[closing + 1:closing + 2] != ':':
48
+ raise ValueError('expected HOST:PORT or [IPv6]:PORT')
49
+ host = value[1:closing]
50
+ port_text = value[closing + 2:]
51
+ if ':' not in host or '[' in host:
52
+ raise ValueError('expected [IPv6]:PORT')
53
+ else:
54
+ host, separator, port_text = value.rpartition(':')
55
+ if not separator or ':' in host or '[' in host or ']' in host:
56
+ raise ValueError('expected HOST:PORT or [IPv6]:PORT')
57
+ if not host:
58
+ raise ValueError('host must not be empty')
59
+ if not port_text or any(character < '0' or character > '9' for character in port_text):
60
+ raise ValueError('port must be a decimal integer from 1 to 65535')
61
+ port = int(port_text)
62
+ if not 1 <= port <= 65535:
63
+ raise ValueError('port must be from 1 to 65535')
64
+ return ConnectionAddress(host, port)
65
+
66
+
67
+ def seconds(value: str) -> float:
68
+ seconds = float(value)
69
+ validate_timeout(seconds)
70
+ return seconds
71
+
72
+
73
+ def timeout(value: str) -> Optional[float]:
74
+ return None if value == 'none' else seconds(value)
75
+
76
+
77
+ def size_limit(value: str) -> SizeLimit:
78
+ size = None if value == 'none' else int(value)
79
+ validate_packet_size_limit(size, 'size limit')
80
+ return SizeLimit(size)
81
+
82
+
83
+ def parse_args() -> CliOptions:
84
+ parser = argparse.ArgumentParser(
85
+ prog='rconx',
86
+ usage='%(prog)s [OPTIONS] HOST:PORT [COMMAND ...]',
87
+ description='Put every local option before HOST:PORT; use [IPv6]:PORT for IPv6 addresses.',
88
+ epilog='Each COMMAND is one complete command string. After HOST:PORT, all arguments are commands, including --help, -i and --. '
89
+ 'Without -i, zero commands check the connection and authentication. With -i, interaction follows the startup commands.',
90
+ )
91
+ password_source = parser.add_mutually_exclusive_group(required=True)
92
+ password_source.add_argument('-p', '--password', metavar='PASSWORD', help='password; an explicitly empty value is allowed')
93
+ password_source.add_argument('--password-env', metavar='NAME', help='read the password from environment variable NAME')
94
+ parser.add_argument('-P', '--preset', choices=[preset.name for preset in RconPreset], default='minecraft', help='RCON exchange preset (default: %(default)s)')
95
+ parser.add_argument('-i', '--interactive', action='store_true', help='read commands interactively after the startup commands')
96
+ parser.add_argument('-t', '--timeout', type=timeout, default=10.0, metavar='SECONDS|none', help='timeout for each connect, authentication or command operation (default: 10 seconds)')
97
+ parser.add_argument('--idle-window', type=seconds, metavar='SECONDS', help='response quiet window; required and valid only for the idle_timeout preset')
98
+ parser.add_argument('--encoding', metavar='NAME', help='password, command and response text encoding (default: core utf8)')
99
+ parser.add_argument('--raw', action='store_true', help='write response bytes; incompatible with -i')
100
+ parser.add_argument('--max-send-packet-size', type=size_limit, metavar='BYTES|none', help='send packet size limit; none disables it (default: selected preset)')
101
+ parser.add_argument('--max-receive-packet-size', type=size_limit, metavar='BYTES|none', help='receive packet size limit; none disables it (default: core limit)')
102
+ parser.add_argument('--max-response-size', type=size_limit, metavar='BYTES|none', help='accumulated response size limit; none disables it (default: core limit)')
103
+ parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__get_self_version()))
104
+ parser.add_argument('items', nargs=argparse.REMAINDER, metavar='HOST:PORT [COMMAND ...]', help='target address followed by zero or more complete command strings')
105
+ arguments = parser.parse_args()
106
+
107
+ if not arguments.items:
108
+ raise ValueError('HOST:PORT is required')
109
+ if (arguments.preset == 'idle_timeout') != (arguments.idle_window is not None):
110
+ raise ValueError('--idle-window is required and valid only for idle_timeout')
111
+ if arguments.raw and arguments.interactive:
112
+ raise ValueError('--raw cannot be combined with --interactive')
113
+ password = os.environ[arguments.password_env] if arguments.password_env is not None else arguments.password
114
+
115
+ return CliOptions(
116
+ address=compute_address(arguments.items[0]),
117
+ password=password,
118
+ commands=arguments.items[1:],
119
+ preset=RconPreset[arguments.preset],
120
+ interactive=arguments.interactive,
121
+ timeout=arguments.timeout,
122
+ idle_window=arguments.idle_window,
123
+ encoding=arguments.encoding,
124
+ raw=arguments.raw,
125
+ max_send_packet_size=arguments.max_send_packet_size,
126
+ max_receive_packet_size=arguments.max_receive_packet_size,
127
+ max_response_size=arguments.max_response_size,
128
+ )
rconx/cli/main.py ADDED
@@ -0,0 +1,78 @@
1
+ import sys
2
+ from typing import Optional
3
+
4
+ from rconx.cli.arguments import CliOptions, parse_args
5
+ from rconx.client.client import RconClient
6
+ from rconx.client.client_options import RconClientOptions
7
+ from rconx.common.content import RconContent
8
+
9
+
10
+ def main():
11
+ options = parse_args()
12
+ with __create_client(options) as client:
13
+ __run(client, options)
14
+
15
+
16
+ def __create_client(options: CliOptions) -> RconClient:
17
+ client_options: RconClientOptions = {}
18
+ if options.encoding is not None:
19
+ client_options['default_encoding'] = options.encoding
20
+ if options.max_send_packet_size is not None:
21
+ client_options['max_send_packet_size'] = options.max_send_packet_size.value
22
+ if options.max_receive_packet_size is not None:
23
+ client_options['max_receive_packet_size'] = options.max_receive_packet_size.value
24
+ if options.max_response_size is not None:
25
+ client_options['max_response_size'] = options.max_response_size.value
26
+ return RconClient.create(
27
+ options.address.host, options.address.port, options.preset,
28
+ first_byte_timeout=options.idle_window,
29
+ **client_options,
30
+ )
31
+
32
+
33
+ def __run(client: RconClient, options: CliOptions):
34
+ client.connect(timeout=options.timeout)
35
+ client.authenticate(options.password, timeout=options.timeout)
36
+
37
+ for option_command in options.commands:
38
+ __execute_command(client, option_command, options)
39
+ if not options.interactive:
40
+ return
41
+
42
+ while True:
43
+ user_command = __read_command()
44
+ if user_command is None:
45
+ return
46
+ if user_command:
47
+ __execute_command(client, user_command, options)
48
+
49
+
50
+ def __execute_command(client: RconClient, command: str, options: CliOptions):
51
+ response = client.execute(command, timeout=options.timeout)
52
+ __write_result(response, options)
53
+
54
+
55
+ def __read_command() -> Optional[str]:
56
+ sys.stderr.write('rconx> ')
57
+ sys.stderr.flush()
58
+ try:
59
+ line = sys.stdin.readline()
60
+ if not line:
61
+ return None
62
+ if line.endswith('\n'):
63
+ line = line[:-1]
64
+ if line.endswith('\r'):
65
+ line = line[:-1]
66
+ except (KeyboardInterrupt, EOFError):
67
+ sys.exit(0)
68
+ return line
69
+
70
+
71
+ def __write_result(response: RconContent, options: CliOptions):
72
+ if options.raw:
73
+ sys.stdout.buffer.write(response.content)
74
+ sys.stdout.buffer.flush()
75
+ else:
76
+ sys.stdout.write(response.text)
77
+ sys.stdout.write('\n')
78
+ sys.stdout.flush()
File without changes
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from rconx.client.operation import AsyncRconOperationContext, RconOperationContext
4
+
5
+
6
+ class AuthenticationHandler(ABC):
7
+ @abstractmethod
8
+ def authenticate(self, context: RconOperationContext, password: bytes):
9
+ raise NotImplementedError
10
+
11
+ @abstractmethod
12
+ async def authenticate_async(self, context: AsyncRconOperationContext, password: bytes):
13
+ raise NotImplementedError
@@ -0,0 +1,69 @@
1
+ from rconx.client.auth.base import AuthenticationHandler
2
+ from rconx.client.operation import AsyncRconOperationContext, RconOperationContext
3
+ from rconx.common.exceptions import RconAuthenticationError, RconProtocolError
4
+ from rconx.common.protocol import Packet, PacketType
5
+
6
+
7
+ class _AuthenticationResponseProcessor:
8
+ def __init__(self, request_id: int):
9
+ self.__request_id = request_id
10
+ self.__received_empty_response = False
11
+ self.__is_complete = False
12
+
13
+ @property
14
+ def is_complete(self) -> bool:
15
+ return self.__is_complete
16
+
17
+ def accept(self, packet: Packet) -> None:
18
+ if packet.payload:
19
+ raise RconProtocolError('authentication response payload length is {}, expected 0 (request id {})'.format(len(packet.payload), packet.request_id))
20
+
21
+ if packet.packet_type == PacketType.auth_response:
22
+ if packet.request_id == -1:
23
+ raise RconAuthenticationError('server rejected authentication for request id {}'.format(self.__request_id))
24
+ if packet.request_id != self.__request_id:
25
+ raise RconProtocolError('unexpected authentication result request id {}, expected {}'.format(packet.request_id, self.__request_id))
26
+
27
+ self.__is_complete = True
28
+ return
29
+
30
+ # Allow at most one empty response-value packet before the authentication result.
31
+ if packet.packet_type != PacketType.response_value:
32
+ raise RconProtocolError(
33
+ 'unexpected authentication response type {}, expected {} (response value) or {} (authentication result)'.format(
34
+ packet.packet_type, PacketType.response_value, PacketType.auth_response,
35
+ )
36
+ )
37
+ if packet.request_id != self.__request_id:
38
+ raise RconProtocolError('unexpected empty authentication response request id {}, expected {}'.format(packet.request_id, self.__request_id))
39
+
40
+ if self.__received_empty_response:
41
+ raise RconProtocolError('received a second empty response-value packet before the authentication result (request id {})'.format(packet.request_id))
42
+
43
+ self.__received_empty_response = True
44
+
45
+
46
+ class SourceAuthenticationHandler(AuthenticationHandler):
47
+ """
48
+ **Not public API**
49
+
50
+ Source RCON authentication for SRCDS, Minecraft and Cuberite.
51
+ Accepts an authentication result directly or after one empty response-value packet.
52
+ """
53
+ def authenticate(self, context: RconOperationContext, password: bytes):
54
+ request = context.prepare_request(PacketType.auth, password)
55
+ response_processor = _AuthenticationResponseProcessor(request.request_id)
56
+
57
+ context.send_packet(request)
58
+
59
+ while not response_processor.is_complete:
60
+ response_processor.accept(context.receive_packet())
61
+
62
+ async def authenticate_async(self, context: AsyncRconOperationContext, password: bytes):
63
+ request = context.prepare_request(PacketType.auth, password)
64
+ response_processor = _AuthenticationResponseProcessor(request.request_id)
65
+
66
+ await context.send_packet(request)
67
+
68
+ while not response_processor.is_complete:
69
+ response_processor.accept(await context.receive_packet())