Skip to content

Commit 4d9c6ad

Browse files
committed
Implemented proof of concept RCON command
1 parent 9b62bde commit 4d9c6ad

File tree

5 files changed

+180
-4
lines changed

5 files changed

+180
-4
lines changed

lgsm/modules/check.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ if [ "$(whoami)" != "root" ]; then
4747
done
4848
fi
4949

50-
allowed_commands_array=(BACKUP CONSOLE DEBUG DETAILS MAP-COMPRESSOR FASTDL MODS-INSTALL MODS-REMOVE MODS-UPDATE MONITOR POST-DETAILS RESTART START STOP TEST-ALERT CHANGE-PASSWORD UPDATE UPDATE-LGSM VALIDATE WIPE)
50+
allowed_commands_array=(BACKUP CONSOLE DEBUG DETAILS MAP-COMPRESSOR FASTDL MODS-INSTALL MODS-REMOVE MODS-UPDATE MONITOR POST-DETAILS RCON RESTART START STOP TEST-ALERT CHANGE-PASSWORD UPDATE UPDATE-LGSM VALIDATE WIPE)
5151
for allowed_command in "${allowed_commands_array[@]}"; do
5252
if [ "${allowed_command}" == "${commandname}" ]; then
5353
check_logs.sh
@@ -61,14 +61,14 @@ for allowed_command in "${allowed_commands_array[@]}"; do
6161
fi
6262
done
6363

64-
allowed_commands_array=(CONSOLE DEBUG MONITOR START STOP)
64+
allowed_commands_array=(CONSOLE DEBUG MONITOR RCON START STOP)
6565
for allowed_command in "${allowed_commands_array[@]}"; do
6666
if [ "${allowed_command}" == "${commandname}" ]; then
6767
check_config.sh
6868
fi
6969
done
7070

71-
allowed_commands_array=(DEBUG DETAILS DEV-QUERY-RAW MONITOR POST_DETAILS START STOP POST-DETAILS)
71+
allowed_commands_array=(DEBUG DETAILS DEV-QUERY-RAW MONITOR POST_DETAILS RCON START STOP POST-DETAILS)
7272
for allowed_command in "${allowed_commands_array[@]}"; do
7373
if [ "${allowed_command}" == "${commandname}" ]; then
7474
if [ -z "${installflag}" ]; then
@@ -86,7 +86,7 @@ for allowed_command in "${allowed_commands_array[@]}"; do
8686
fi
8787
done
8888

89-
allowed_commands_array=(CHANGE-PASSWORD DETAILS MONITOR START STOP UPDATE VALIDATE POST-DETAILS)
89+
allowed_commands_array=(CHANGE-PASSWORD DETAILS MONITOR RCON START STOP UPDATE VALIDATE POST-DETAILS)
9090
for allowed_command in "${allowed_commands_array[@]}"; do
9191
if [ "${allowed_command}" == "${commandname}" ]; then
9292
check_status.sh

lgsm/modules/command_rcon.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/bin/bash
2+
# LinuxGSM command_rcon.sh module
3+
# Author: Daniel Gibbs
4+
# Contributors: http://linuxgsm.com/contrib
5+
# Website: https://linuxgsm.com
6+
# Description: Send rcon commands to different gameservers.
7+
8+
commandname="RCON"
9+
commandaction="Rcon"
10+
moduleselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")"
11+
fn_firstcommand_set
12+
13+
check.sh
14+
if [ "${status}" == "0" ]; then
15+
fn_print_error_nl "Server not running"
16+
fn_script_log_error "Failed to access: Server not running"
17+
if fn_prompt_yn "Do you want to start the server?" Y; then
18+
exitbypass=1
19+
command_start.sh
20+
fi
21+
fi
22+
23+
24+
if [ -n "${userinput2}" ]; then
25+
rconcommandtosend="${userinput2}"
26+
else
27+
fn_print_header
28+
fn_print_information_nl "Send a RCON command to the server."
29+
echo ""
30+
rconcommandtosend=$(fn_prompt_message "RCON command: ")
31+
fi
32+
33+
fn_print_dots "Sending RCON command to server: \"${rconcommandtosend}\""
34+
35+
if [ ! -f "${modulesdir}/rcon.py" ]; then
36+
fn_fetch_file_github "lgsm/modules" "rcon.py" "${modulesdir}" "chmodx" "norun" "noforce" "nohash"
37+
fi
38+
39+
"${modulesdir}"/rcon.py -a "${telnetip}" -p "${rconport}" -P "${rconpassword}" -c "${rconcommandtosend}" > /dev/null 2>&1
40+
41+
fn_print_ok_nl "Sending RCON command to server: \"${rconcommandtosend}\""
42+
fn_script_log_pass "RCON command \"${rconcommandtosend}\" sent to server"
43+
44+
core_exit.sh

lgsm/modules/core_getopt.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ cmd_monitor=("m;monitor" "command_monitor.sh" "Check server status and restart i
2424
cmd_skeleton=("sk;skeleton" "command_skeleton.sh" "Create a skeleton directory.")
2525
cmd_sponsor=("s;sponsor" "command_sponsor.sh" "Sponsorship options.")
2626
cmd_send=("sd;send" "command_send.sh" "Send command to game server console.")
27+
cmd_rcon=("rc;rcon" "command_rcon.sh" "Send RCON command to game server.")
2728
# Console servers only.
2829
cmd_console=("c;console" "command_console.sh" "Access server console.")
2930
cmd_debug=("d;debug" "command_debug.sh" "Start server directly in your terminal.")
@@ -92,6 +93,11 @@ if [ "${consoleinteract}" == "yes" ]; then
9293
currentopt+=("${cmd_send[@]}")
9394
fi
9495

96+
# RCON command.
97+
# TODO: Add RCON type to all _default.cfg files [Source RCON Protocol / Other Protocols?! / None?!]
98+
# TODO: then add a check in the rcon command to use the appropriate protocol
99+
currentopt+=("${cmd_rcon[@]}")
100+
95101
## Game server exclusive commands.
96102

97103
# FastDL command.

lgsm/modules/core_modules.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ command_send.sh() {
181181
fn_fetch_module
182182
}
183183

184+
command_rcon.sh() {
185+
modulefile="${FUNCNAME[0]}"
186+
fn_fetch_module
187+
}
188+
184189
# Checks
185190

186191
check.sh() {
@@ -739,6 +744,11 @@ install_gsquery.sh() {
739744
fn_fetch_module
740745
}
741746

747+
install_rcon.sh() {
748+
modulefile="${FUNCNAME[0]}"
749+
fn_fetch_module
750+
}
751+
742752
install_gslt.sh() {
743753
modulefile="${FUNCNAME[0]}"
744754
fn_fetch_module

lgsm/modules/rcon.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#! /usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
# LinuxGSM rcon.py module
4+
# Author: MicLieg
5+
# Contributors: http://linuxgsm.com/contrib
6+
# Website: https://linuxgsm.com
7+
# Description: Allows sending RCON commands to different gameservers.
8+
9+
import argparse
10+
import socket
11+
import struct
12+
import sys
13+
14+
15+
class PacketTypes:
16+
LOGIN = 3
17+
COMMAND = 2
18+
19+
20+
class Rcon:
21+
22+
def __init__(self, arguments):
23+
self.arguments = arguments
24+
self.connection = None
25+
26+
def __enter__(self):
27+
self.connect_to_server()
28+
return self
29+
30+
def __exit__(self, exc_type, exc_val, exc_tb):
31+
if self.connection:
32+
self.connection.close()
33+
34+
@staticmethod
35+
def fatal_error(error_message, error_code):
36+
sys.stderr.write(f'ERROR: {error_code} {error_message}\n')
37+
sys.exit(error_code)
38+
39+
@staticmethod
40+
def exit_success(success_message=''):
41+
sys.stdout.write(f'OK: {success_message}\n')
42+
sys.exit(0)
43+
44+
def connect_to_server(self):
45+
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
46+
self.connection.settimeout(self.arguments.timeout)
47+
48+
try:
49+
self.connection.connect((self.arguments.address, self.arguments.port))
50+
except socket.timeout:
51+
self.fatal_error('Request timed out', 1)
52+
except Exception as e:
53+
self.fatal_error(f'Unable to connect: {e}', 1)
54+
55+
def send_packet(self, request_id, packet_type, data):
56+
# Packet structure follows the Source RCON Protocol: size, request ID, type, data, two null bytes
57+
packet = (
58+
struct.pack('<l', request_id)
59+
+ struct.pack('<l', packet_type)
60+
+ data.encode('utf8') + b'\x00\x00'
61+
)
62+
try:
63+
self.connection.send(struct.pack('<l', len(packet)) + packet)
64+
except socket.error as e:
65+
self.fatal_error(f'Failed to send packet: {e}', 2)
66+
67+
def receive_packet(self):
68+
try:
69+
response = self.connection.recv(self.arguments.buffer)
70+
return response
71+
except socket.error as e:
72+
self.fatal_error(f'Failed to receive response: {e}', 3)
73+
74+
def login(self):
75+
self.send_packet(1, PacketTypes.LOGIN, self.arguments.password)
76+
response = self.receive_packet()
77+
if response:
78+
size, id_response, type_response = struct.unpack('<l', response[:4]), struct.unpack('<l', response[
79+
4:8]), struct.unpack(
80+
'<l', response[8:12])
81+
82+
if id_response[0] == -1:
83+
self.fatal_error('Login to RCON failed', 4)
84+
else:
85+
self.fatal_error('No response received for login', 4)
86+
87+
def send_command(self):
88+
self.send_packet(2, PacketTypes.COMMAND, self.arguments.command)
89+
response = self.receive_packet()
90+
if response:
91+
response_message = response[12:-2].decode('utf-8') # Stripping trailing null bytes
92+
self.exit_success(str(response_message))
93+
else:
94+
self.fatal_error('No response received for command', 5)
95+
96+
97+
def parse_args():
98+
parser = argparse.ArgumentParser(description='Sends RCON commands to Minecraft servers.')
99+
parser.add_argument('-a', '--address', type=str, required=True, help='The server IP address.')
100+
parser.add_argument('-p', '--port', type=int, required=True, help='The server port.')
101+
parser.add_argument('-P', '--password', type=str, required=True, help='The RCON password.')
102+
parser.add_argument('-c', '--command', type=str, required=True, help='The RCON command to send.')
103+
parser.add_argument('-t', '--timeout', type=int, default=5, help='The timeout for server response.')
104+
parser.add_argument('-b', '--buffer', type=int, default=4096, help='The buffer length for server response.')
105+
return parser.parse_args()
106+
107+
108+
def main():
109+
arguments = parse_args()
110+
with Rcon(arguments) as rcon:
111+
rcon.login()
112+
rcon.send_command()
113+
114+
115+
if __name__ == '__main__':
116+
main()

0 commit comments

Comments
 (0)