Última atividade 1736893123

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

onyx_online's Avatar onyx-and-iris revisou este gist 1736893124. Ir para a revisão

1 file changed, 6 insertions, 2 deletions

rcon.py

@@ -23,6 +23,7 @@
23 23 # SOFTWARE.
24 24
25 25 import argparse
26 + import re
26 27 import socket
27 28
28 29
@@ -156,6 +157,9 @@ def main():
156 157 None
157 158 """
158 159
160 + def cleaned_response(resp: str) -> str:
161 + return re.sub(r"\^[0-9]", "", resp)
162 +
159 163 args = parse_args()
160 164
161 165 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
@@ -165,7 +169,7 @@ def main():
165 169
166 170 if not args.interactive:
167 171 if resp := send(sock, request, args.host, args.port, args.cmd):
168 - print(resp)
172 + print(cleaned_response(resp))
169 173 return
170 174
171 175 while cmd := input("cmd: ").strip():
@@ -173,7 +177,7 @@ def main():
173 177 break
174 178
175 179 if resp := send(sock, request, args.host, args.port, cmd):
176 - print(resp)
180 + print(cleaned_response(resp))
177 181
178 182
179 183 if __name__ == "__main__":

onyx_online's Avatar onyx-and-iris revisou este gist 1736632704. Ir para a revisão

1 file changed, 61 insertions, 11 deletions

rcon.py

@@ -31,7 +31,23 @@ class Packet:
31 31
32 32
33 33 class Request(Packet):
34 - """Class used to encode a 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 + """
35 51
36 52 def __init__(self, password: str):
37 53 self.password = password
@@ -44,8 +60,24 @@ class Request(Packet):
44 60
45 61
46 62 class ResponseBuilder(Packet):
47 - """Class used to build and decode a response packet.
48 - The response may be built from multiple fragments.
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.
49 81 """
50 82
51 83 def __init__(self):
@@ -65,17 +97,24 @@ class ResponseBuilder(Packet):
65 97
66 98
67 99 def send(sock: socket.socket, request: Request, host: str, port: int, cmd: str) -> str:
68 - """Send a single rcon command to the server and return the response.
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.
69 105
70 106 Args:
71 - sock (socket.socket): UDP socket object
72 - request (Request): reference to a Request object
73 - host (str): hostname or IP address
74 - port (int): port number
75 - cmd (str): the rcon command to send
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.
76 112
77 113 Returns:
78 - str: the response from the server
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.
79 118 """
80 119 sock.sendto(request.encode(cmd), (host, port))
81 120
@@ -105,7 +144,18 @@ def parse_args() -> argparse.Namespace:
105 144
106 145
107 146 def main():
108 - """Fire command in one-shot mode or interactive mode."""
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 +
109 159 args = parse_args()
110 160
111 161 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:

onyx_online's Avatar onyx-and-iris revisou este gist 1736632279. Ir para a revisão

1 file changed, 4 insertions, 4 deletions

rcon.py

@@ -108,13 +108,13 @@ def main():
108 108 """Fire command in one-shot mode or interactive mode."""
109 109 args = parse_args()
110 110
111 - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
112 - s.settimeout(args.timeout)
111 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
112 + sock.settimeout(args.timeout)
113 113
114 114 request = Request(args.password)
115 115
116 116 if not args.interactive:
117 - if resp := send(s, request, args.host, args.port, args.cmd):
117 + if resp := send(sock, request, args.host, args.port, args.cmd):
118 118 print(resp)
119 119 return
120 120
@@ -122,7 +122,7 @@ def main():
122 122 if cmd == "Q":
123 123 break
124 124
125 - if resp := send(s, request, args.host, args.port, cmd):
125 + if resp := send(sock, request, args.host, args.port, cmd):
126 126 print(resp)
127 127
128 128

onyx_online's Avatar onyx_online revisou este gist 1736632007. Ir para a revisão

1 file changed, 130 insertions

rcon.py(arquivo criado)

@@ -0,0 +1,130 @@
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 +
25 + import argparse
26 + import socket
27 +
28 +
29 + class Packet:
30 + MAGIC = bytearray([0xFF, 0xFF, 0xFF, 0xFF])
31 +
32 +
33 + class Request(Packet):
34 + """Class used to encode a request packet."""
35 +
36 + def __init__(self, password: str):
37 + self.password = password
38 +
39 + def _header(self) -> bytes:
40 + return Packet.MAGIC + b"rcon"
41 +
42 + def encode(self, cmd: str) -> bytes:
43 + return self._header() + f" {self.password} {cmd}".encode()
44 +
45 +
46 + class ResponseBuilder(Packet):
47 + """Class used to build and decode a response packet.
48 + The response may be built from multiple fragments.
49 + """
50 +
51 + def __init__(self):
52 + self._fragments = bytearray()
53 +
54 + def _header(self) -> bytes:
55 + return Packet.MAGIC + b"print\n"
56 +
57 + def is_valid_fragment(self, fragment: bytes) -> bool:
58 + return fragment.startswith(self._header())
59 +
60 + def add_fragment(self, fragment: bytes):
61 + self._fragments.extend(fragment.removeprefix(self._header()))
62 +
63 + def build(self) -> str:
64 + return self._fragments.decode()
65 +
66 +
67 + def send(sock: socket.socket, request: Request, host: str, port: int, cmd: str) -> str:
68 + """Send a single rcon command to the server and return the response.
69 +
70 + Args:
71 + sock (socket.socket): UDP socket object
72 + request (Request): reference to a Request object
73 + host (str): hostname or IP address
74 + port (int): port number
75 + cmd (str): the rcon command to send
76 +
77 + Returns:
78 + str: the response from the server
79 + """
80 + sock.sendto(request.encode(cmd), (host, port))
81 +
82 + response_builder = ResponseBuilder()
83 + while True:
84 + try:
85 + data, _ = sock.recvfrom(4096)
86 + except socket.timeout:
87 + break
88 +
89 + if response_builder.is_valid_fragment(data):
90 + response_builder.add_fragment(data)
91 +
92 + return response_builder.build()
93 +
94 +
95 + def parse_args() -> argparse.Namespace:
96 + parser = argparse.ArgumentParser()
97 + parser.add_argument("--host", default="localhost")
98 + parser.add_argument("--port", type=int, default=27960)
99 + parser.add_argument("--password", default="secret")
100 + parser.add_argument("--timeout", type=float, default=0.2)
101 + parser.add_argument("--interactive", action="store_true")
102 + parser.add_argument("cmd", nargs="?", default="status")
103 + args = parser.parse_args()
104 + return args
105 +
106 +
107 + def main():
108 + """Fire command in one-shot mode or interactive mode."""
109 + args = parse_args()
110 +
111 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
112 + s.settimeout(args.timeout)
113 +
114 + request = Request(args.password)
115 +
116 + if not args.interactive:
117 + if resp := send(s, request, args.host, args.port, args.cmd):
118 + print(resp)
119 + return
120 +
121 + while cmd := input("cmd: ").strip():
122 + if cmd == "Q":
123 + break
124 +
125 + if resp := send(s, request, args.host, args.port, cmd):
126 + print(resp)
127 +
128 +
129 + if __name__ == "__main__":
130 + main()
Próximo Anterior