-
Notifications
You must be signed in to change notification settings - Fork 1
/
cutercon.py
executable file
·225 lines (172 loc) · 7.09 KB
/
cutercon.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#!/usr/bin/env python3
from enum import IntEnum
from queue import Queue
import socket
import select
import struct
import sys
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget, QLineEdit, QTextEdit
# Set these constants before running the script.
HOST = "192.168.0.10"
PASSWORD = "password"
PORT = 25575
# Set the maximum number of recent commands to be displayed.
MAX_QUEUE_SIZE = 50
class PacketType(IntEnum):
"""
Packet type field is a 32-bit little endian integer, indicating the purpose of the packet.
"""
# Incoming payload is the output of the command. Commands can return nothing.
CommandResponse = 0
# Outgoing payload is command to be run, e.g. "time set 0".
Command = 2
# Outgoing payload is the RCON password set by the server.
# Server returns packet w/ the same request ID upon success.
# The return packet will be type 2 (Command).
# A response with a request ID of -1 indicates a wrong password.
Login = 3
class CuterconException(Exception):
pass
class Cutercon(object):
"""
Remote Console Client for Minecraft: Java Edition
"""
cuterconSocket = None
def __init__(self, host, password, port=25575):
self.host = host
self.password = password
self.port = port
# If using Context Manager.
def __enter__(self):
self.connect()
return self
# If using Context Manager.
# Guarantees socket is closed upon program exit.
def __exit__(self, exceptionType, exceptionValue, exceptionTraceback):
self.disconnect()
print("Disconnecting from %s:%i..." % (self.host, self.port))
def connect(self):
"""
Create new socket w/ the address & protocol family (AF_INET is IPv4).
SOCK_STREAM is a type of socket known as a stream socket,
providing two-way communication between client and server.
Call disconnect() after finishing with the socket!
"""
self.cuterconSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.cuterconSocket.connect((self.host, self.port))
self._sendData(PacketType.Login, self.password)
def disconnect(self):
"""
Always close socket after finishing!
"""
if self.cuterconSocket is not None:
self.cuterconSocket.close()
self.cuterconSocket = None
def _readData(self, length):
data = b""
# Ensures all incoming bytes are read.
while len(data) < length:
data += self.cuterconSocket.recv(length - len(data))
return data
def _sendData(self, outgoingPacketType, outgoingPacketData):
if self.cuterconSocket is None:
raise CuterconException("A socket must be connected before sending data.")
INITIAL_BYTE = 0
EMPTY_TWO_BYTE_STRING = b"\x00\x00"
# Must first send a request packet.
outgoingPayload = (
struct.pack("<ii", INITIAL_BYTE, outgoingPacketType)
+ outgoingPacketData.encode("utf8")
+ EMPTY_TWO_BYTE_STRING
)
outgoingPayloadLength = struct.pack("<i", len(outgoingPayload))
self.cuterconSocket.send(outgoingPayloadLength + outgoingPayload)
# Now read response packets.
incomingData = "" # Must be defined outside loop for concatenation.
while True:
# First, read a packet.
BYTES_IN_32BIT_INT = 4
BYTES_IN_ID_AND_TYPE_FIELD = 8
# Note that unpack returns a tuple even for one item.
# The empty items are "thrown" away.
(incomingPayloadLength,) = struct.unpack("<i", self._readData(BYTES_IN_32BIT_INT))
incomingPayload = self._readData(incomingPayloadLength)
incomingPayloadID, incomingPayloadType = struct.unpack("<ii", incomingPayload[:BYTES_IN_ID_AND_TYPE_FIELD])
incomingDataPartial, incomingPayloadPadding = (
incomingPayload[BYTES_IN_ID_AND_TYPE_FIELD:-2],
incomingPayload[-2:],
)
# Sanity checking
if incomingPayloadPadding != b"\x00\x00":
raise CuterconException("Incorrect packet padding!")
WRONG_PASSWORD_ID = -1
if incomingPayloadID == WRONG_PASSWORD_ID:
raise CuterconException("Authentication failed: Wrong password!")
# Store response
incomingData += incomingDataPartial.decode("utf8")
# Return response if no more data to receive
TIMEOUT = 0.0 # A timeout value of zero specifies a poll and never blocks.
if len(select.select([self.cuterconSocket], [], [], TIMEOUT)[0]) == 0:
return incomingData
def command(self, command):
result = self._sendData(PacketType.Command, command)
return result
class CuterconQt(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Cutercon")
self.resize(500, 500) # width, height
layout = QVBoxLayout()
self.setLayout(layout)
self.outputField = QTextEdit()
self.outputField.setReadOnly(True)
self.inputField = QLineEdit()
self.inputField.setPlaceholderText("Enter command...")
layout.addWidget(self.outputField)
layout.addWidget(self.inputField)
self.inputField.returnPressed.connect(self.sendCommand)
self.inputField.returnPressed.connect(self.printText)
self.commandQueue = Queue(MAX_QUEUE_SIZE)
self.connection = Cutercon(HOST, PASSWORD, PORT)
try:
self.connection.connect()
self.printText("Connected to %s:%i..." % (HOST, PORT))
except ConnectionRefusedError:
print("The server refused the connection!")
except ConnectionError as error:
print(error)
def __enter__(self):
return self
def __exit__(self, exceptionType, exceptionValue, exceptionTraceback):
self.connection.disconnect()
print("Disconnecting from %s:%i..." % (HOST, PORT))
def sendCommand(self):
commandText = self.inputField.text()
try:
self.connection.command(commandText)
except (ConnectionResetError, ConnectionAbortedError):
print("Server connection terminated, perhaps it crashed or was stopped...")
def addToQueue(self, command):
if self.commandQueue.full():
self.commandQueue.get()
self.commandQueue.put(command)
else:
self.commandQueue.put(command)
def printText(self, programText=None):
if programText is None:
inputText = self.inputField.text()
else:
inputText = programText
self.addToQueue(inputText)
# Erase sent command to improve UX.
self.inputField.clear()
# Clear screen before printing queue.
self.outputField.clear()
# Now print to window.
for command in list(self.commandQueue.queue):
self.outputField.append("$ {0}".format(command))
if __name__ == "__main__":
app = QApplication([])
with CuterconQt() as window:
window.show()
sys.exit(app.exec())