Skip to content

Commit 3c29659

Browse files
committed
Fixed issue where outgoing packet limits would not be enforced, cleaned/added this info to the docs, and added some type hinting
1 parent 3e48962 commit 3c29659

File tree

14 files changed

+239
-90
lines changed

14 files changed

+239
-90
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ __pycache__/
2323
# Python distribution archive directory/PyInstaller spec files
2424

2525
dist/
26+
build/
2627
*.spec
2728
*.egg-info
2829

@@ -34,3 +35,7 @@ dist/
3435

3536
docs/build/
3637
docs/source/_build
38+
39+
# General dummy files:
40+
41+
test.py

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ if rcon.login("password"):
116116

117117
Be sure to also check out the [mctools PyPi page](https://pypi.org/project/mctools/) for more information.
118118

119+
# Bug Reports
120+
121+
If you encounter a bug or any other event that does not seem normal,
122+
then please open an issue, or email me personally.
123+
I will be sure to get back to you as soon as possible.
124+
125+
Your feedback and reports are appreciated!
126+
Your comments and issues are an excellent way to correct issues with mctools.
127+
119128
# Contributing
120129

121130
Pull requests are welcome and encouraged :) ! If you want to see a feature in mctools, a PR will be the quickest
@@ -126,6 +135,18 @@ if rcon.login("password"):
126135

127136
# Changelog
128137

138+
## 1.1.2
139+
140+
Fixed an issue where RCON command size handling was broken.
141+
Before, the remote server would kill the connection if a command is too large(Bigger than 1460 bytes).
142+
We now raise a new exception, 'RCONLengthError' and refuse to send the packet if the command is too big,
143+
thus keeping the connection alive. You can optionally disable this check, although this is not recommended.
144+
145+
The documentation has also been updated to make this limitation more clear,
146+
as well as correcting some minor errors, mostly correcting examples in the formatting documentation.
147+
148+
We also added some type hinting, as some IDEs were complaining about type mismatches.
149+
129150
## 1.1.1
130151

131152
Fixed an issue where clients hang when the connection is closed by the remote host.

docs/source/conf.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,19 @@
1212
#
1313
import os
1414
import sys
15+
1516
sys.path.insert(0, os.path.abspath('../..'))
1617

18+
import mctools
1719

1820
# -- Project information -----------------------------------------------------
1921

2022
project = 'Minecraft-Connection-Tools'
21-
copyright = '2020, Owen Cochell'
23+
copyright = '2021, Owen Cochell'
2224
author = 'Owen Cochell'
2325

2426
# The full version, including alpha/beta/rc tags
25-
release = '1.1.0'
27+
release = mctools.__version__
2628

2729

2830
# -- General configuration ---------------------------------------------------
@@ -58,7 +60,7 @@
5860
'github_user': 'Owen-Cochell',
5961
'github_repo': 'mctools',
6062
'github_button': True,
61-
'github_type': 'watch',
63+
'github_type': 'star',
6264
'extra_nav_links': {
6365
'Github Page': 'https://github.com/Owen-Cochell/mctools',
6466
'PyPi Page': 'https://pypi.org/project/mctools/',

docs/source/format.rst

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,22 @@ Here is an example of a description in ChatObject notation:
146146
147147
{'extra': [{'bold': True,
148148
'color': 'yellow',
149-
'text': 'This is bold and yellow!'},
149+
'text': 'This is bold and yellow!'},
150150
151151
{'color': 'gold',
152-
'text': ' Just gold. New line!\n'},
152+
'text': ' Just gold. New line!\n'},
153153
154154
{'color': 'white',
155-
'italics': True,
156-
'text': 'We are on a new line, '},
155+
'italics': True,
156+
'text': 'We are on a new line, '},
157157
158158
{'color': 'green',
159-
'text': 'and we love the color green.'},
159+
'text': 'and we love the color green.'},
160160
161161
{'color': 'white',
162-
'text': '.'}],
162+
'text': '.'}]},
163163
164-
'text': ''},
164+
'text': ''}
165165
166166
As you can see, this makes reading and parsing the content difficult. ChatObjectFormatter fixes this problem
167167
by converting the dictionary into a single string, which makes reading and parsing the data much easier.
@@ -177,7 +177,7 @@ Have a look at this example sample player list:
177177

178178
.. code-block:: python
179179
180-
'players': {'max': 5000,
180+
{'players': {'max': 5000,
181181
'online': 723,
182182
'sample': [{'id': '00000000-0000-0000-0000-000000000000',
183183
'name': 'We are a server.'},
@@ -194,7 +194,7 @@ Have a look at this example sample player list:
194194
{'id': '00000000-0000-0000-0000-000000000000',
195195
'name': 'Here is one of them:'},
196196
{'id': '2ef8ad56-ec35-46e7-b90c-8172386d3fe7',
197-
'name': 'MinecraftPLayer1'}]}
197+
'name': 'MinecraftPLayer1'}]}}
198198
199199
If you look, there is a message encoded in this sample list, with one valid player.
200200
The message has a null UUID, which is how SampleDescriptionFormatter determines if a user list is actually
@@ -204,11 +204,11 @@ After the formatting operation is complete, the *player* sub-dictionary will loo
204204

205205
.. code-block:: python
206206
207-
'players': {'max': 5000,
207+
{'players': {'max': 5000,
208208
'online': 723,
209209
'sample': [{'id': '2ef8ad56-ec35-46e7-b90c-8172386d3fe7',
210210
'name': 'MinecraftPLayer1'}],
211-
'message': 'We are a server.\n\nCheck out our Twitter!\n\nWe have really great players!\n\nHere is one of them:'}
211+
'message': 'We are a server.\n\nCheck out our Twitter!\n\nWe have really great players!\n\nHere is one of them:'}}
212212
213213
Now, the sample only contains valid players, and the message is stored under a separate key named 'message'.
214214
This allows us to accurately determine who is really playing, and view the message with no extra processing.
@@ -330,7 +330,7 @@ get
330330
This will return the list of formatters used by FormatterCollection. The information on a formatter is stored in a
331331
sublist with the following format:
332332

333-
.. code-block:: python
333+
.. code-block::
334334
335335
[Formatter Instance,
336336
Commands to match,

docs/source/mcli.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This section defines general usage for mcli.py
2121
However, if you downloaded the source code or the source distribution without installing,
2222
the you will call mcli as a python file:
2323

24-
.. code-block:: python
24+
.. code-block::
2525
2626
python3 mcli.py [arguments]
2727
@@ -226,25 +226,25 @@ Screenshots
226226

227227
Here are some screenshots of mcli in action:
228228

229-
RCON Usage
230-
----------
229+
RCON
230+
----
231231

232232
Interactive RCON session
233233

234234
.. image:: rcon.png
235235
:alt: rcon screenshot
236236

237-
Query usage
238-
-----------
237+
Query
238+
------
239239

240240
Fetching full statistics via Query
241241

242242
.. image:: query.png
243243
:alt: query screenshot
244244
:height: 300px
245245

246-
Ping usage
247-
----------
246+
Ping
247+
-----
248248

249249
Pinging Minecraft server and fetching statistics
250250

docs/source/ping.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ You can do this like so:
4747
4848
from mctools import PINGClient
4949
50-
ping = PINGClient('mc.server.net', proto_num=[PROTOCOL NUMBER])
50+
ping = PINGClient('mc.server.net', proto_num=PROTOCOL_NUMBER)
5151
5252
This allows PINGClient to emulate certain versions of the Minecraft client. By default, we use protocol number 0,
5353
which means we are inquiring on which protocol version we should use.
@@ -95,12 +95,12 @@ The contents of the dictionary *should* be in the following format:
9595
'online': 1,
9696
'sample': [
9797
{'id': 'fbf11fd0-5b74-490c-adc4-91febe9de2ae',
98-
'name': 'MinecraftPlayer'}]},
99-
'message': ''
98+
'name': 'MinecraftPlayer'}],
99+
'message': ''},
100100
'time': 0.09879999993245292,
101101
'version': {
102102
'name': '1.15.2',
103-
'protocol': 578}
103+
'protocol': 578},
104104
'favicon': 'data:image/png;base64,<data>'}
105105
106106
The *description* field is the message of the day.

docs/source/query.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ The *plugins* filed contains a list of plugins used.
137137
This is not used by the vanilla server, however community servers
138138
such as Bukkit and Spigot use this value. Most servers follow this format:
139139

140-
.. code-block:: python
140+
.. code-block::
141141
142142
[SERVER_MOD_NAME[: PLUGIN_NAME(; PLUGIN_NAME...)]]
143143

docs/source/rcon.rst

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ The function will return the response in string format from the server, formatte
8989

9090
.. note::
9191

92-
You can disable the authentication check by passing 'no_auth=True' to the command function, which will disable
92+
You can disable the authentication check by passing 'no_auth=True' to the command function, which will disable
9393
the check for this command. Be aware that if the server refuses to serve you, then a RCONAuthenticationError
9494
exception will be raised.
9595

@@ -108,17 +108,58 @@ The command sent will broadcast "Hello RCON!" to every player on the server.
108108
when issued over RCON, so this is usually a normal operation. It can also mean that the server doesn't understand
109109
the command issued.
110110

111-
RCON Packet Fragmentation
112-
_________________________
111+
RCON Outgoing Packet length
112+
___________________________
113113

114-
Sometimes, the RCON server will send fragmented packets.
115-
This is because RCON has a maximum packet size of 4096 bytes.
116-
RCONClient will automatically handle packet fragmentation for you.
114+
The RCON Protocol has an outgoing(client to server) packet size limitation of 1460 bytes.
115+
Taking into account the mandatory information we have to send(request ID, type, padding, ect.),
116+
the maximum command size that can be sent is 1446 bytes.
117+
118+
This limitation unfortunately has no workaround,
119+
and is an issue with the RCON protocol, and therefore beyond our control.
120+
mctools does implement a length check to make sure outgoing packets are not too big.
121+
122+
If an outgoing packet is too big, and the length check is enabled,
123+
then an 'RCONLengthError' exception will be raised, and the packet will not be sent.
124+
This ensures that any nasty side effects of going over the outgoing limit will be avoided,
125+
thus keeping the connection in a stable state.
126+
127+
You can optionally disable the outgoing length check by passing 'length_check=False' to the command method.
128+
129+
.. warning::
130+
131+
Disabling outgoing length checks is not recommended! Doing so could mess up the state of your client!
132+
133+
Here is an example of disabling outgoing length checks:
117134

118-
If the packet received is 4096 bytes in length, then we will assume the packet is fragmented.
119-
We send a junk packet to the server, and read packets until the server acknowledges the junk packet.
120-
RCON ensures that all packets are sent in the order that they are received, meaning that once the server responds to
121-
the junk packet, then we can be sure that we have all of the relevant packets.
135+
.. code-block:: python
136+
137+
# Lets send a HUGE command:
138+
# (Assume 'huge_command' is a string containing a command larger than 1446 bytes)
139+
140+
resp = rcon.command(huge_command, length_check=False)
141+
142+
This will prevent the 'RCONLengthError' exception from being raised,
143+
and mctools will send the large packet normally.
144+
145+
If a large packet is sent to the RCON server, then some nasty things could occur.
146+
The most likely is that the server will forcefully close the connection,
147+
although other unsavory events could occur.
148+
This is why we recommend keeping the outgoing length check enabled.
149+
150+
RCON Incoming Packet Fragmentation
151+
__________________________________
152+
153+
Sometimes, the RCON server will send fragmented packets.
154+
This is because RCON has an incoming(server to client) maximum packet size of 4096 bytes.
155+
RCONClient will automatically handle incoming packet fragmentation for you.
156+
157+
If the incoming packet is 4096 bytes in length, then we will assume the packet is fragmented.
158+
If this is the case, then mctools sends a junk packet to the server,
159+
and reads packets until the server acknowledges the junk packet.
160+
The RCON protocol ensures that all packets are sent in the order that they are received,
161+
meaning that once the server responds to the junk packet,
162+
then we can be sure that we have all of the relevant packets.
122163
We then concatenate the packets we received, and return it as one.
123164

124165
However, you can disable the check by passing 'frag_check=False' to the command method.
@@ -135,7 +176,7 @@ Here is an example of disabling RCON packet fragmentation:
135176
136177
resp = rcon.command("help", frag_check=False)
137178
138-
This will return the content of the first 4096 bytes. Any subsequent to 'command' or 'raw_send' will return the rest
179+
This will return the content of the first 4096 bytes. Any subsequent call to 'command' or 'raw_send' will return the rest
139180
of the fragmented packets. This means that you will have incomplete content, and subsequent calls will return
140181
irrelevant information. Unless you have a reason for this, it is recommended to keep packet fragmentation enabled.
141182

mctools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55
# Define some metadata here:
66

7-
__version__ = '1.1.1'
7+
__version__ = '1.1.2'
88
__author__ = 'Owen Cochell'

mctools/encoding.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import struct
77
import json
88

9-
from mctools.errors import RCONMalformedPacketError
9+
from typing import Tuple, Union
10+
11+
from mctools.errors import RCONMalformedPacketError, PINGMalformedPacketError
1012

1113

1214
MAP = {''}
@@ -326,7 +328,7 @@ def encode(data):
326328
return byts
327329

328330
@staticmethod
329-
def decode(byts):
331+
def decode(byts) -> Tuple[dict, None, str]:
330332

331333
"""
332334
Decodes a ping packet.
@@ -355,19 +357,22 @@ def decode(byts):
355357

356358
# Working with some other packet type:
357359

358-
return None, None, "pong"
360+
return {}, None, "pong"
361+
362+
raise PINGMalformedPacketError("Invalid packet type! Must be 1 or 0, not {}!".format(pack_type))
359363

360364
@staticmethod
361-
def encode_varint(num):
365+
def encode_varint(num: int) -> bytes:
362366

363367
"""
364368
Encodes a number into varint bytes.
365369
366370
:param num: Number to encode
371+
:type num: int
367372
:return: Bytes
368373
"""
369374

370-
byts = b''
375+
byts: bytes = b''
371376
remaining = num
372377

373378
# Iterating over the content, and doing the necessary bitwise stuff
@@ -378,14 +383,16 @@ def encode_varint(num):
378383

379384
# Found our stuff, exit
380385

381-
byts = byts + struct.pack("!B", remaining)
382-
return byts
386+
break
383387

384388
byts = byts + struct.pack("!B", remaining & 0x7F | 0x80)
385389
remaining >>= 7
386390

391+
byts = byts + struct.pack("!B", remaining)
392+
return byts
393+
387394
@staticmethod
388-
def decode_varint(byts):
395+
def decode_varint(byts) -> Tuple[int, int]:
389396

390397
"""
391398
Decodes varint bytes into an integer.
@@ -415,7 +422,7 @@ def decode_varint(byts):
415422
return result, b + 1
416423

417424
@staticmethod
418-
def decode_sock(sock):
425+
def decode_sock(sock) -> int:
419426

420427
"""
421428
Decodes a var(int/long) of variable length or value.

0 commit comments

Comments
 (0)