Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rcon): add RCON command #4488

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lgsm/modules/check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ if [ "$(whoami)" != "root" ]; then
done
fi

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)
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)
for allowed_command in "${allowed_commands_array[@]}"; do
if [ "${allowed_command}" == "${commandname}" ]; then
check_logs.sh
Expand All @@ -61,14 +61,14 @@ for allowed_command in "${allowed_commands_array[@]}"; do
fi
done

allowed_commands_array=(CONSOLE DEBUG MONITOR START STOP)
allowed_commands_array=(CONSOLE DEBUG MONITOR RCON START STOP)
for allowed_command in "${allowed_commands_array[@]}"; do
if [ "${allowed_command}" == "${commandname}" ]; then
check_config.sh
fi
done

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

allowed_commands_array=(CHANGE-PASSWORD DETAILS MONITOR START STOP UPDATE VALIDATE POST-DETAILS)
allowed_commands_array=(CHANGE-PASSWORD DETAILS MONITOR RCON START STOP UPDATE VALIDATE POST-DETAILS)
for allowed_command in "${allowed_commands_array[@]}"; do
if [ "${allowed_command}" == "${commandname}" ]; then
check_status.sh
Expand Down
44 changes: 44 additions & 0 deletions lgsm/modules/command_rcon.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
# LinuxGSM command_rcon.sh module
# Author: Daniel Gibbs
# Contributors: http://linuxgsm.com/contrib
# Website: https://linuxgsm.com
# Description: Send rcon commands to different gameservers.

commandname="RCON"
commandaction="Rcon"
moduleselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")"
fn_firstcommand_set

check.sh
if [ "${status}" == "0" ]; then
fn_print_error_nl "Server not running"
fn_script_log_error "Failed to access: Server not running"
if fn_prompt_yn "Do you want to start the server?" Y; then
exitbypass=1
command_start.sh
fi
fi


if [ -n "${userinput2}" ]; then
rconcommandtosend="${userinput2}"
else
fn_print_header
fn_print_information_nl "Send a RCON command to the server."
echo ""
rconcommandtosend=$(fn_prompt_message "RCON command: ")
fi

fn_print_dots "Sending RCON command to server: \"${rconcommandtosend}\""

if [ ! -f "${modulesdir}/rcon.py" ]; then
fn_fetch_file_github "lgsm/modules" "rcon.py" "${modulesdir}" "chmodx" "norun" "noforce" "nohash"
fi

"${modulesdir}"/rcon.py -a "${telnetip}" -p "${rconport}" -P "${rconpassword}" -c "${rconcommandtosend}" > /dev/null 2>&1

fn_print_ok_nl "Sending RCON command to server: \"${rconcommandtosend}\""
fn_script_log_pass "RCON command \"${rconcommandtosend}\" sent to server"

core_exit.sh
6 changes: 6 additions & 0 deletions lgsm/modules/core_getopt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ cmd_monitor=("m;monitor" "command_monitor.sh" "Check server status and restart i
cmd_skeleton=("sk;skeleton" "command_skeleton.sh" "Create a skeleton directory.")
cmd_sponsor=("s;sponsor" "command_sponsor.sh" "Sponsorship options.")
cmd_send=("sd;send" "command_send.sh" "Send command to game server console.")
cmd_rcon=("rc;rcon" "command_rcon.sh" "Send RCON command to game server.")
# Console servers only.
cmd_console=("c;console" "command_console.sh" "Access server console.")
cmd_debug=("d;debug" "command_debug.sh" "Start server directly in your terminal.")
Expand Down Expand Up @@ -92,6 +93,11 @@ if [ "${consoleinteract}" == "yes" ]; then
currentopt+=("${cmd_send[@]}")
fi

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

## Game server exclusive commands.

# FastDL command.
Expand Down
10 changes: 10 additions & 0 deletions lgsm/modules/core_modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ command_send.sh() {
fn_fetch_module
}

command_rcon.sh() {
modulefile="${FUNCNAME[0]}"
fn_fetch_module
}

# Checks

check.sh() {
Expand Down Expand Up @@ -739,6 +744,11 @@ install_gsquery.sh() {
fn_fetch_module
}

install_rcon.sh() {
modulefile="${FUNCNAME[0]}"
fn_fetch_module
}

install_gslt.sh() {
modulefile="${FUNCNAME[0]}"
fn_fetch_module
Expand Down
116 changes: 116 additions & 0 deletions lgsm/modules/rcon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# LinuxGSM rcon.py module
# Author: MicLieg
# Contributors: http://linuxgsm.com/contrib
# Website: https://linuxgsm.com
# Description: Allows sending RCON commands to different gameservers.

import argparse
import socket
import struct
import sys


class PacketTypes:
LOGIN = 3
COMMAND = 2


class Rcon:

def __init__(self, arguments):
self.arguments = arguments
self.connection = None

def __enter__(self):
self.connect_to_server()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
self.connection.close()

@staticmethod
def fatal_error(error_message, error_code):
sys.stderr.write(f'ERROR: {error_code} {error_message}\n')
sys.exit(error_code)

@staticmethod
def exit_success(success_message=''):
sys.stdout.write(f'OK: {success_message}\n')
sys.exit(0)

def connect_to_server(self):
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connection.settimeout(self.arguments.timeout)

try:
self.connection.connect((self.arguments.address, self.arguments.port))
except socket.timeout:
self.fatal_error('Request timed out', 1)
except Exception as e:
self.fatal_error(f'Unable to connect: {e}', 1)

def send_packet(self, request_id, packet_type, data):
# Packet structure follows the Source RCON Protocol: size, request ID, type, data, two null bytes
packet = (
struct.pack('<l', request_id)
+ struct.pack('<l', packet_type)
+ data.encode('utf8') + b'\x00\x00'
)
try:
self.connection.send(struct.pack('<l', len(packet)) + packet)
except socket.error as e:
self.fatal_error(f'Failed to send packet: {e}', 2)

def receive_packet(self):
try:
response = self.connection.recv(self.arguments.buffer)
return response
except socket.error as e:
self.fatal_error(f'Failed to receive response: {e}', 3)

def login(self):
self.send_packet(1, PacketTypes.LOGIN, self.arguments.password)
response = self.receive_packet()
if response:
size, id_response, type_response = struct.unpack('<l', response[:4]), struct.unpack('<l', response[
4:8]), struct.unpack(
'<l', response[8:12])

if id_response[0] == -1:
self.fatal_error('Login to RCON failed', 4)
else:
self.fatal_error('No response received for login', 4)

def send_command(self):
self.send_packet(2, PacketTypes.COMMAND, self.arguments.command)
response = self.receive_packet()
if response:
response_message = response[12:-2].decode('utf-8') # Stripping trailing null bytes
self.exit_success(str(response_message))
else:
self.fatal_error('No response received for command', 5)


def parse_args():
parser = argparse.ArgumentParser(description='Sends RCON commands to Minecraft servers.')
parser.add_argument('-a', '--address', type=str, required=True, help='The server IP address.')
parser.add_argument('-p', '--port', type=int, required=True, help='The server port.')
parser.add_argument('-P', '--password', type=str, required=True, help='The RCON password.')
parser.add_argument('-c', '--command', type=str, required=True, help='The RCON command to send.')
parser.add_argument('-t', '--timeout', type=int, default=5, help='The timeout for server response.')
parser.add_argument('-b', '--buffer', type=int, default=4096, help='The buffer length for server response.')
return parser.parse_args()


def main():
arguments = parse_args()
with Rcon(arguments) as rcon:
rcon.login()
rcon.send_command()


if __name__ == '__main__':
main()