update name & add docs
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
build:
|
build:
|
||||||
python -m zipapp src -m "main:main" -o rcon-client.pyz -p "/usr/bin/env python3"
|
python -m zipapp src -m "main:main" -o wircon.pyz -p "/usr/bin/env python3"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf __pycache__
|
rm -rf **/__pycache__
|
||||||
rm -f *.pyz
|
rm -f *.pyz
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# WiRCON
|
||||||
|
An over-the-wire RCON client with support for interactive and script-friendly usage. Implements the RCON protocol using raw TCP packets: no third-party libraries and can bundle to a single Python zip archive for easy packaging into a Docker container.
|
||||||
|
|
||||||
|
Built to replace [Tiiffi/mcrcon](https://github.com/Tiiffi/mcrcon) as part of my [minecraft-docker](https://gitea.jovan04.com/evan/minecraft-docker) project because I wanted something easier to use and modify for my own purposes, and I wanted a better understanding of the RCON protocol. See `src/rcon.py` for my RCON implementation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Interactive REPL with color & arrow-key support, command history, and Ctrl-C/Ctrl-D handling matching normal shell behavior
|
||||||
|
- Single-command mode (`-e`) for scripted/one-off invocations; flag can be invoked multiple times to run multiple commands in sequence
|
||||||
|
- Script mode (`-E`) for running arbitrary Python against an authenticated connection (see below)
|
||||||
|
- Correct multi-packet response reassembly (handles the Notchian server's non-conformant fragmentation via invalid-type sentinel packets - see [Protocol notes](#protocol-notes))
|
||||||
|
- Zero third-party dependencies - Python standard library only
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- Python 3.10+
|
||||||
|
- A Minecraft server with `enable-rcon=true` set in `server.properties`
|
||||||
|
- A Linux or MacOS (untested) environment (for Windows, see [Known limitations](#known-limitations))
|
||||||
|
- GNU Make recommended for building from source or for development
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
First, clone or download this repository.
|
||||||
|
|
||||||
|
### Run from source
|
||||||
|
Run directly with Python 3.10+ (see [Usage](#usage)):
|
||||||
|
```sh
|
||||||
|
python src/main.py [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the standalone `.pyz` Python zip archive
|
||||||
|
Useful for dropping into a Docker container. Still requires Python to run. Run the executable directly in place of `python main.py`:
|
||||||
|
```sh
|
||||||
|
make build
|
||||||
|
./wircon.pyz [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Can be run interactively to view commands and their responses, or hands-free (e.g. for use in automations). Includes a help menu for additional usage information:
|
||||||
|
```sh
|
||||||
|
python main.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive mode
|
||||||
|
For interactive mode, run the program with no arguments:
|
||||||
|
```sh
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
A hostname, password, and optional port (default: 25575) can be specified ahead of time for automatic login:
|
||||||
|
```sh
|
||||||
|
python main.py -H localhost -P 25585 -p VerySecurePassword
|
||||||
|
```
|
||||||
|
|
||||||
|
### Single command
|
||||||
|
Run with the `-e` flag to execute a single command. Multiple instances of the `-e` flag can be passed to run multiple commands, in the order they're passed.
|
||||||
|
```sh
|
||||||
|
# ex: minimum usage
|
||||||
|
python main.py -H <host> -p <password> -e "list"
|
||||||
|
# ex: send a restart message, kick all players, and stop the server
|
||||||
|
python main.py -H <host> -p <password> -e "say server is restarting!" -e "kick @p server restart" -e "stop"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script mode
|
||||||
|
Works the same from the `.pyz` - the archive still runs on a real Python interpreter, so it can read and execute your script file from disk at runtime just like the source version.
|
||||||
|
```sh
|
||||||
|
python main.py -H <host> -p <password> -E path/to/script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing scripts
|
||||||
|
A script passed to `-E` must define:
|
||||||
|
```python
|
||||||
|
def run(sock):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`sock` is an already-connected, already-authenticated socket — use `rcon.send_command(sock, "...")` to run commands.
|
||||||
|
|
||||||
|
|
||||||
|
Example (`scripts/restart.py`):
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
from rcon import send_command
|
||||||
|
|
||||||
|
def run(sock):
|
||||||
|
send_command(sock, "say server is restarting!")
|
||||||
|
send_command(sock, "save-all")
|
||||||
|
time.sleep(10)
|
||||||
|
send_command(sock, "stop")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker integration
|
||||||
|
TBD, maybe
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
- Server-side command failures (typos, bad arguments) are not distinguishable from successes at the protocol level - RCON responses are plain text with no status field. See [Protocol notes](#protocol-notes). Thus, even when server-side errors occur, `-e` or `-E` mode execution will continue executing.
|
||||||
|
- Windows is currently unsupported because it doesn't include the necessary GNU Readline system library or the corresponding Python module. See [Protocol notes](#protocol-notes).
|
||||||
|
- No unit tests at the moment. We test in production, baby! 🚀
|
||||||
|
|
||||||
|
## Protocol notes
|
||||||
|
I used the following documentation sites as reference for developing this project:
|
||||||
|
- [Valve developer community](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol)
|
||||||
|
- [Minecraft.wiki](https://minecraft.wiki/w/RCON)
|
||||||
|
- [Archive of wiki.vg](https://c4k3.github.io/wiki.vg/Rcon.html#Packet_Format)
|
||||||
|
- Various pages in the [Python standard library documentation](https://docs.python.org/3/)
|
||||||
|
|
||||||
|
I primarily made this RCON client as an educational exercise for myself. Here are some interesting tidbits, quirks, and other things I learned while developing it.
|
||||||
|
|
||||||
|
1. The Notchian Minecraft server doesn't properly implement multi-packet responses, meaning that there can be ambiguity about where the response to one command ends and another begins. To handle this, we send two packets for each command: one that contains the command, and another sentinel packet that has an invalid packet type (e.g. 200). Because the server is guaranteed to respond to packets in the order it receives them, we wait until we get a response from the second packet (tracked by the request id field); everything before it was part of the first packet that contained our actual command.
|
||||||
|
2. RCON really isn't a great protocol - it has a few notable issues. First, traffic is entirely unencrypted, which makes it vulnerable to packet-sniffing attacks. Since your password is sent to the server in plain-text when logging in, someone listening to network traffic can trivially impersonate a server administrator. Second, it's purely a raw-text-communication protocol - no status codes, no information beyond pure text output of a command. This means that error handling is basically impossible: if a `save-all` fails, the RCON client has no way of knowing without parsing the response string, which is brittle and difficult to do on a large scale.
|
||||||
|
3. Distributions of Python (typically on Linux and MacOS) contain an optional package called [`readline`](https://docs.python.org/3/library/readline.html). This package interfaces with the GNU Readline library, overwriting the default `input()` built-in to provide arrow-key support and command history. However, because GNU Readline is GPL-licensed, it can't be distributed with precompiled Python binaries (like `uv` and Conda installs) or closed-source operating systems like MacOS. Such distributions often link to a similar library instead, `libedit`; this library has a few minor differences in how it handles ANSI control characters (like those used for colored prompts). To sum things up: color codes like that need to be flushed using a visible character afterwards (like a space).
|
||||||
|
4. For Windows users: Windows operating systems don't include the GNU Readline library (or a comparable alternative that Python integrates with). Many distributions of Python for Windows aren't even compiled with the `readline` module, so one can't just download a 'GNU Readline for Windows'. If you comment out the `import readline` statement at the top of `src/utils.py`, the client will probably work just fine, but you won't be able to use the arrow keys in interactive mode. The protocol itself (in `src/rcon.py`) is platform-agnostic, so you can write your own wrapper for it.
|
||||||
|
5. This program dynamically imports user-provided Python files - I don't know if it's possible to compile everything to a standalone platform-specific binary. I found a happy medium with a `.pyz` Python zip archive that Python just executes like a regular package; it still lets me drop a single file into a docker container. However, if one wanted to create something executable without a Python installation, this might pose some problems.
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import time
|
||||||
|
from rcon import send_command
|
||||||
|
|
||||||
|
def run(sock):
|
||||||
|
send_command(sock, "say server is restarting!")
|
||||||
|
send_command(sock, "save-all")
|
||||||
|
time.sleep(10)
|
||||||
|
send_command(sock, "stop")
|
||||||
|
|
||||||
+1
-1
@@ -16,7 +16,7 @@ def parse_args():
|
|||||||
execute_arg_group.add_argument("-e", "--execute-command", help="Send a single command on the RCON server. Can be passed multiple times to run multiple commands. Does not abort on server-side command failures. Requires -H and -p to be specified. Mutually exclusive with -E.", metavar="COMMAND", action='append')
|
execute_arg_group.add_argument("-e", "--execute-command", help="Send a single command on the RCON server. Can be passed multiple times to run multiple commands. Does not abort on server-side command failures. Requires -H and -p to be specified. Mutually exclusive with -E.", metavar="COMMAND", action='append')
|
||||||
execute_arg_group.add_argument("-E", "--execute-file", help="Execute a Python file. Requires -H and -p to be specified. Mutually exclusive with -e.", metavar="FILEPATH")
|
execute_arg_group.add_argument("-E", "--execute-file", help="Execute a Python file. Requires -H and -p to be specified. Mutually exclusive with -e.", metavar="FILEPATH")
|
||||||
|
|
||||||
parser.add_argument("--echo", help="Echo a command before sending it. Does nothing when neither -e nor -E are not present.", action="store_true")
|
parser.add_argument("--echo", help="Echo a command before sending it. Does nothing when neither -e nor -E is present.", action="store_true")
|
||||||
parser.add_argument("--socket-timeout", help="Set the timeout for the socket when waiting to receive data. Default 30 seconds.", type=float, metavar="TIMEOUT_SECONDS")
|
parser.add_argument("--socket-timeout", help="Set the timeout for the socket when waiting to receive data. Default 30 seconds.", type=float, metavar="TIMEOUT_SECONDS")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
Reference in New Issue
Block a user