Utoljára aktív 1736893123

Use this script to send rcon commands to a game server supporting Q3 Rcon

Revízió cea79570483b18109be00220eb178da578e818b2

rcon.py Eredeti
1#!/usr/bin/env python3
2
3# MIT License
4#
5# Copyright (c) 2025 Onyx and Iris
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to deal
9# in the Software without restriction, including without limitation the rights
10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11# copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in all
15# copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23# SOFTWARE.
24
25import argparse
26import socket
27
28
29class Packet:
30 MAGIC = bytearray([0xFF, 0xFF, 0xFF, 0xFF])
31
32
33class Request(Packet):
34 """
35 Class used to encode a request packet for remote console (RCON) communication.
36
37 Attributes:
38 password (str): The password used for authentication with the RCON server.
39
40 Methods:
41 _header() -> bytes:
42 Generates the header for the RCON packet.
43
44 encode(cmd: str) -> bytes:
45 Encodes the command into a byte string suitable for sending to the RCON server.
46 Args:
47 cmd (str): The command to be sent to the RCON server.
48 Returns:
49 bytes: The encoded command as a byte string.
50 """
51
52 def __init__(self, password: str):
53 self.password = password
54
55 def _header(self) -> bytes:
56 return Packet.MAGIC + b"rcon"
57
58 def encode(self, cmd: str) -> bytes:
59 return self._header() + f" {self.password} {cmd}".encode()
60
61
62class ResponseBuilder(Packet):
63 """
64 Class used to build and decode a response packet from multiple fragments.
65
66 Attributes:
67 _fragments (bytearray): A bytearray to store the concatenated fragments.
68
69 Methods:
70 _header() -> bytes:
71 Returns the header bytes that each fragment should start with.
72
73 is_valid_fragment(fragment: bytes) -> bool:
74 Checks if the given fragment starts with the expected header.
75
76 add_fragment(fragment: bytes):
77 Adds a fragment to the internal storage after removing the header.
78
79 build() -> str:
80 Decodes and returns the concatenated fragments as a string.
81 """
82
83 def __init__(self):
84 self._fragments = bytearray()
85
86 def _header(self) -> bytes:
87 return Packet.MAGIC + b"print\n"
88
89 def is_valid_fragment(self, fragment: bytes) -> bool:
90 return fragment.startswith(self._header())
91
92 def add_fragment(self, fragment: bytes):
93 self._fragments.extend(fragment.removeprefix(self._header()))
94
95 def build(self) -> str:
96 return self._fragments.decode()
97
98
99def send(sock: socket.socket, request: Request, host: str, port: int, cmd: str) -> str:
100 """
101 Send a single RCON (Remote Console) command to the server and return the response.
102
103 This function sends a command to a game server using the RCON protocol over a UDP socket.
104 It waits for the server's response, collects it in fragments if necessary, and returns the complete response.
105
106 Args:
107 sock (socket.socket): The UDP socket object used to send and receive data.
108 request (Request): An instance of the Request class used to encode the command.
109 host (str): The hostname or IP address of the server.
110 port (int): The port number on which the server is listening.
111 cmd (str): The RCON command to be sent to the server.
112
113 Returns:
114 str: The complete response from the server after processing all received fragments.
115
116 Raises:
117 socket.timeout: If the socket times out while waiting for a response from the server.
118 """
119 sock.sendto(request.encode(cmd), (host, port))
120
121 response_builder = ResponseBuilder()
122 while True:
123 try:
124 data, _ = sock.recvfrom(4096)
125 except socket.timeout:
126 break
127
128 if response_builder.is_valid_fragment(data):
129 response_builder.add_fragment(data)
130
131 return response_builder.build()
132
133
134def parse_args() -> argparse.Namespace:
135 parser = argparse.ArgumentParser()
136 parser.add_argument("--host", default="localhost")
137 parser.add_argument("--port", type=int, default=27960)
138 parser.add_argument("--password", default="secret")
139 parser.add_argument("--timeout", type=float, default=0.2)
140 parser.add_argument("--interactive", action="store_true")
141 parser.add_argument("cmd", nargs="?", default="status")
142 args = parser.parse_args()
143 return args
144
145
146def main():
147 """
148 Main function to handle command-line arguments and interact with a remote server using RCON protocol.
149 This function parses command-line arguments, creates a UDP socket, and sends requests to a remote server.
150 It supports both interactive and non-interactive modes.
151 In non-interactive mode, it sends a single command to the server and prints the response.
152 In interactive mode, it continuously prompts the user for commands until the user quits by entering 'Q'.
153 Args:
154 None
155 Returns:
156 None
157 """
158
159 args = parse_args()
160
161 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
162 sock.settimeout(args.timeout)
163
164 request = Request(args.password)
165
166 if not args.interactive:
167 if resp := send(sock, request, args.host, args.port, args.cmd):
168 print(resp)
169 return
170
171 while cmd := input("cmd: ").strip():
172 if cmd == "Q":
173 break
174
175 if resp := send(sock, request, args.host, args.port, cmd):
176 print(resp)
177
178
179if __name__ == "__main__":
180 main()
181