minestat 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +8 -0
- data/ChangeLog.md +55 -0
- data/License.txt +674 -0
- data/ReadMe.md +60 -0
- data/example.rb +18 -0
- data/lib/minestat.rb +404 -169
- metadata +16 -8
data/lib/minestat.rb
CHANGED
@@ -22,66 +22,120 @@ require 'resolv'
|
|
22
22
|
require 'socket'
|
23
23
|
require 'timeout'
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
# @author Lloyd Dilley
|
26
|
+
|
27
|
+
# Provides a Ruby interface for polling the status of Minecraft servers
|
27
28
|
class MineStat
|
28
29
|
# MineStat version
|
29
|
-
VERSION = "
|
30
|
+
VERSION = "3.0.0"
|
31
|
+
|
30
32
|
# Number of values expected from server
|
31
33
|
NUM_FIELDS = 6
|
32
|
-
|
34
|
+
private_constant :NUM_FIELDS
|
35
|
+
|
36
|
+
# Number of values expected from a 1.8b - 1.3 server
|
33
37
|
NUM_FIELDS_BETA = 3
|
38
|
+
private_constant :NUM_FIELDS_BETA
|
39
|
+
|
34
40
|
# Maximum number of bytes a varint can be
|
35
41
|
MAX_VARINT_SIZE = 5
|
42
|
+
private_constant :MAX_VARINT_SIZE
|
43
|
+
|
36
44
|
# Default TCP port
|
37
45
|
DEFAULT_TCP_PORT = 25565
|
46
|
+
|
38
47
|
# Bedrock/Pocket Edition default UDP port
|
39
48
|
DEFAULT_BEDROCK_PORT = 19132
|
49
|
+
|
40
50
|
# Default TCP/UDP timeout in seconds
|
41
51
|
DEFAULT_TIMEOUT = 5
|
52
|
+
|
42
53
|
# Bedrock/Pocket Edition packet offset in bytes (1 + 8 + 8 + 16 + 2)
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
54
|
+
# Unconnected pong (0x1C) = 1 byte
|
55
|
+
# Timestamp as a long = 8 bytes
|
56
|
+
# Server GUID as a long = 8 bytes
|
57
|
+
# Magic number = 16 bytes
|
58
|
+
# String ID length = 2 bytes
|
48
59
|
BEDROCK_PACKET_OFFSET = 35
|
49
|
-
|
50
|
-
|
51
|
-
#
|
60
|
+
private_constant :BEDROCK_PACKET_OFFSET
|
61
|
+
|
62
|
+
# UT3/GS4 query handshake packet size in bytes (1 + 4 + 13)
|
63
|
+
# Handshake (0x09) = 1 byte
|
64
|
+
# Session ID = 4 bytes
|
65
|
+
# Challenge token = variable null-terminated string up to 13 bytes(?)
|
66
|
+
QUERY_HANDSHAKE_SIZE = 18
|
67
|
+
private_constant :QUERY_HANDSHAKE_SIZE
|
68
|
+
|
69
|
+
# UT3/GS4 query handshake packet offset for challenge token in bytes (1 + 4)
|
70
|
+
# Handshake (0x09) = 1 byte
|
71
|
+
# Session ID = 4 bytes
|
72
|
+
QUERY_HANDSHAKE_OFFSET = 5
|
73
|
+
private_constant :QUERY_HANDSHAKE_OFFSET
|
74
|
+
|
75
|
+
# UT3/GS4 query full stat packet offset in bytes (1 + 4 + 11)
|
76
|
+
# Stat (0x00) = 1 byte
|
77
|
+
# Session ID = 4 bytes
|
78
|
+
# Padding = 11 bytes
|
79
|
+
QUERY_STAT_OFFSET = 16
|
80
|
+
private_constant :QUERY_STAT_OFFSET
|
81
|
+
|
82
|
+
# These constants represent the result of a server request
|
52
83
|
module Retval
|
53
84
|
# The server ping completed successfully
|
54
85
|
SUCCESS = 0
|
86
|
+
|
55
87
|
# The server ping failed due to a connection error
|
56
88
|
CONNFAIL = -1
|
89
|
+
|
57
90
|
# The server ping failed due to a connection time out
|
58
91
|
TIMEOUT = -2
|
92
|
+
|
59
93
|
# The server ping failed for an unknown reason
|
60
94
|
UNKNOWN = -3
|
61
95
|
end
|
62
96
|
|
63
|
-
|
64
|
-
# Stores constants that represent the different kinds of server
|
65
|
-
# list pings/requests that a Minecraft server might expect when
|
66
|
-
# being polled for status information.
|
97
|
+
# These constants represent the various protocols used when requesting server data
|
67
98
|
module Request
|
68
99
|
# Try everything
|
69
100
|
NONE = -1
|
101
|
+
|
70
102
|
# Server versions 1.8b to 1.3
|
71
103
|
BETA = 0
|
104
|
+
|
72
105
|
# Server versions 1.4 to 1.5
|
73
106
|
LEGACY = 1
|
107
|
+
|
74
108
|
# Server version 1.6
|
75
109
|
EXTENDED = 2
|
110
|
+
|
76
111
|
# Server versions 1.7 to latest
|
77
112
|
JSON = 3
|
113
|
+
|
78
114
|
# Bedrock/Pocket Edition
|
79
115
|
BEDROCK = 4
|
116
|
+
|
117
|
+
# Unreal Tournament 3/GameSpy 4 query
|
118
|
+
QUERY = 5
|
80
119
|
end
|
81
120
|
|
82
|
-
|
83
|
-
#
|
84
|
-
|
121
|
+
# Instantiates a MineStat object and polls the specified server for information
|
122
|
+
# @param address [String] Minecraft server address
|
123
|
+
# @param port [Integer] Minecraft server TCP or UDP port
|
124
|
+
# @param timeout [Integer] TCP/UDP timeout in seconds
|
125
|
+
# @param request_type [Request] Protocol used to poll a Minecraft server
|
126
|
+
# @param debug [Boolean] Enable or disable error output
|
127
|
+
# @return [MineStat] A MineStat object
|
128
|
+
# @example Simply connect to an address
|
129
|
+
# ms = MineStat.new("minecraft.frag.land")
|
130
|
+
# @example Connect to an address on a certain TCP or UDP port
|
131
|
+
# ms = MineStat.new("minecraft.frag.land", 25567)
|
132
|
+
# @example Same as above example and additionally includes a timeout in seconds
|
133
|
+
# ms = MineStat.new("minecraft.frag.land", 25567, 3)
|
134
|
+
# @example Same as above example and additionally includes an explicit protocol to use
|
135
|
+
# ms = MineStat.new("minecraft.frag.land", 25567, 3, MineStat::Request::QUERY)
|
136
|
+
# @example Connect to a Bedrock server and enable debug mode
|
137
|
+
# ms = MineStat.new("minecraft.frag.land", 19132, 3, MineStat::Request::BEDROCK, true)
|
138
|
+
def initialize(address, port = DEFAULT_TCP_PORT, timeout = DEFAULT_TIMEOUT, request_type = Request::NONE, debug = false)
|
85
139
|
@address = address # address of server
|
86
140
|
@port = port # TCP/UDP port of server
|
87
141
|
@online # online or offline?
|
@@ -91,6 +145,8 @@ class MineStat
|
|
91
145
|
@stripped_motd # message of the day without formatting
|
92
146
|
@current_players # current number of players online
|
93
147
|
@max_players # maximum player capacity
|
148
|
+
@player_list # list of players (UT3/GS4 query only)
|
149
|
+
@plugin_list # list of plugins (UT3/GS4 query only)
|
94
150
|
@protocol # protocol level
|
95
151
|
@json_data # JSON data for 1.7 queries
|
96
152
|
@favicon_b64 # base64-encoded favicon possibly contained in JSON 1.7 responses
|
@@ -101,13 +157,18 @@ class MineStat
|
|
101
157
|
@request_type # protocol version
|
102
158
|
@connection_status # status of connection ("Success", "Fail", "Timeout", or "Unknown")
|
103
159
|
@try_all = false # try all protocols?
|
160
|
+
@debug = debug # debug mode
|
104
161
|
|
105
162
|
@try_all = true if request_type == Request::NONE
|
106
163
|
resolve_srv(address, port)
|
107
|
-
set_connection_status(attempt_protocols())
|
164
|
+
set_connection_status(attempt_protocols(request_type))
|
108
165
|
end
|
109
166
|
|
110
167
|
# Attempts to resolve SRV records
|
168
|
+
# @param address [String] Minecraft server address
|
169
|
+
# @param port [Integer] Minecraft server TCP or UDP port
|
170
|
+
# @return [Boolean] Whether or not SRV resolution was successful
|
171
|
+
# @since 2.3.0
|
111
172
|
def resolve_srv(address, port)
|
112
173
|
begin
|
113
174
|
resolver = Resolv::DNS.new
|
@@ -115,13 +176,19 @@ class MineStat
|
|
115
176
|
@address = res.target.to_s # SRV target
|
116
177
|
@port = res.port.to_i # SRV port
|
117
178
|
rescue => exception # primarily catch Resolv::ResolvError and revert if unable to resolve SRV record(s)
|
179
|
+
$stderr.puts exception if @debug
|
118
180
|
@address = address
|
119
181
|
@port = port
|
182
|
+
return false
|
120
183
|
end
|
184
|
+
return true
|
121
185
|
end
|
186
|
+
private :resolve_srv
|
122
187
|
|
123
188
|
# Attempts the use of various protocols
|
124
|
-
|
189
|
+
# @param request_type [Request] Protocol used to poll a Minecraft server
|
190
|
+
# @return [Retval] Return value
|
191
|
+
def attempt_protocols(request_type)
|
125
192
|
case request_type
|
126
193
|
when Request::BETA
|
127
194
|
retval = beta_request()
|
@@ -133,6 +200,8 @@ class MineStat
|
|
133
200
|
retval = json_request()
|
134
201
|
when Request::BEDROCK
|
135
202
|
retval = bedrock_request()
|
203
|
+
when Request::QUERY
|
204
|
+
retval = query_request()
|
136
205
|
else
|
137
206
|
# Attempt various ping requests in a particular order. If the
|
138
207
|
# connection fails, there is no reason to continue with subsequent
|
@@ -157,17 +226,24 @@ class MineStat
|
|
157
226
|
unless @online || retval == Retval::SUCCESS
|
158
227
|
retval = bedrock_request()
|
159
228
|
end
|
229
|
+
# UT3/GS4 query
|
230
|
+
unless @online || retval == Retval::SUCCESS
|
231
|
+
retval = query_request()
|
232
|
+
end
|
160
233
|
end
|
161
234
|
return retval
|
162
235
|
end
|
236
|
+
private :attempt_protocols
|
163
237
|
|
164
238
|
# Sets connection status
|
239
|
+
# @param retval [Retval] Return value
|
165
240
|
def set_connection_status(retval)
|
166
241
|
@connection_status = "Success" if @online || retval == Retval::SUCCESS
|
167
242
|
@connection_status = "Fail" if retval == Retval::CONNFAIL
|
168
243
|
@connection_status = "Timeout" if retval == Retval::TIMEOUT
|
169
244
|
@connection_status = "Unknown" if retval == Retval::UNKNOWN
|
170
245
|
end
|
246
|
+
private :set_connection_status
|
171
247
|
|
172
248
|
# Strips message of the day formatting characters
|
173
249
|
def strip_motd()
|
@@ -187,13 +263,13 @@ class MineStat
|
|
187
263
|
@stripped_motd = @stripped_motd.force_encoding('UTF-8')
|
188
264
|
@stripped_motd = @stripped_motd.gsub(/§./, "")
|
189
265
|
end
|
266
|
+
private :strip_motd
|
190
267
|
|
191
|
-
##
|
192
268
|
# Establishes a connection to the Minecraft server
|
193
269
|
def connect()
|
194
270
|
begin
|
195
|
-
if @request_type == Request::BEDROCK || @request_type == "Bedrock/Pocket Edition"
|
196
|
-
@port = DEFAULT_BEDROCK_PORT if @port == DEFAULT_TCP_PORT && @try_all
|
271
|
+
if @request_type == Request::BEDROCK || @request_type == "Bedrock/Pocket Edition" || @request_type == "UT3/GS4 Query"
|
272
|
+
@port = DEFAULT_BEDROCK_PORT if @port == DEFAULT_TCP_PORT && @request_type != "UT3/GS4 Query" && @try_all
|
197
273
|
start_time = Time.now
|
198
274
|
@server = UDPSocket.new
|
199
275
|
@server.connect(@address, @port)
|
@@ -205,15 +281,18 @@ class MineStat
|
|
205
281
|
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
206
282
|
return Retval::CONNFAIL
|
207
283
|
rescue => exception
|
208
|
-
|
284
|
+
$stderr.puts exception if @debug
|
209
285
|
return Retval::UNKNOWN
|
210
286
|
end
|
211
287
|
return Retval::SUCCESS
|
212
288
|
end
|
289
|
+
private :connect
|
213
290
|
|
214
|
-
#
|
215
|
-
|
291
|
+
# Validates server response based on beginning of the packet
|
292
|
+
# @return [String, Retval] Raw data received from a Minecraft server and the return value
|
293
|
+
def check_response()
|
216
294
|
data = nil
|
295
|
+
retval = nil
|
217
296
|
begin
|
218
297
|
if @request_type == "Bedrock/Pocket Edition"
|
219
298
|
if @server.recv(1, Socket::MSG_PEEK).unpack('C').first == 0x1C # unconnected pong packet
|
@@ -222,7 +301,15 @@ class MineStat
|
|
222
301
|
@server.close
|
223
302
|
else
|
224
303
|
@server.close
|
225
|
-
|
304
|
+
retval = Retval::UNKNOWN
|
305
|
+
end
|
306
|
+
elsif @request_type == "UT3/GS4 Query"
|
307
|
+
if @server.recv(1, Socket::MSG_PEEK).unpack('C').first == 0x00 # stat packet
|
308
|
+
data = @server.recv(4096)[QUERY_STAT_OFFSET..-1]
|
309
|
+
@server.close
|
310
|
+
else
|
311
|
+
@server.close
|
312
|
+
retval = Retval::UNKNOWN
|
226
313
|
end
|
227
314
|
else # SLP
|
228
315
|
if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
|
@@ -231,20 +318,31 @@ class MineStat
|
|
231
318
|
@server.close
|
232
319
|
else
|
233
320
|
@server.close
|
234
|
-
|
321
|
+
retval = Retval::UNKNOWN
|
235
322
|
end
|
236
323
|
end
|
237
|
-
rescue
|
238
|
-
|
239
|
-
|
240
|
-
return Retval::UNKNOWN
|
324
|
+
rescue => exception
|
325
|
+
$stderr.puts exception if @debug
|
326
|
+
return nil, Retval::UNKNOWN
|
241
327
|
end
|
328
|
+
retval = Retval::UNKNOWN if data == nil || data.empty?
|
329
|
+
return data, retval
|
330
|
+
end
|
331
|
+
private :check_response
|
242
332
|
|
243
|
-
|
244
|
-
|
245
|
-
|
333
|
+
# Populates object fields after retrieving data from a Minecraft server
|
334
|
+
# @param delimiter [String] Delimiter used to split a string into an array
|
335
|
+
# @param is_beta [Boolean] Whether or not the Minecraft server is using version 1.8b to 1.3
|
336
|
+
def parse_data(delimiter, is_beta = false)
|
337
|
+
data, retval = check_response()
|
338
|
+
return retval if retval == Retval::UNKNOWN
|
246
339
|
|
247
|
-
server_info =
|
340
|
+
server_info = nil
|
341
|
+
if @request_type == "UT3/GS4 Query"
|
342
|
+
server_info = data.split("\x00\x00\x01player_\x00\x00")
|
343
|
+
else
|
344
|
+
server_info = data.split(delimiter)
|
345
|
+
end
|
248
346
|
if is_beta
|
249
347
|
if server_info != nil && server_info.length >= NUM_FIELDS_BETA
|
250
348
|
@version = ">=1.8b/1.3" # since server does not return version, set it
|
@@ -269,6 +367,25 @@ class MineStat
|
|
269
367
|
else
|
270
368
|
return Retval::UNKNOWN
|
271
369
|
end
|
370
|
+
elsif @request_type == "UT3/GS4 Query"
|
371
|
+
if server_info != nil
|
372
|
+
@player_list = server_info[1].split(delimiter) unless server_info[1].nil? || server_info[1].empty?
|
373
|
+
server_info = Hash[*server_info[0].split(delimiter).flatten(1)]
|
374
|
+
@version = server_info["version"]
|
375
|
+
@motd = server_info["hostname"]
|
376
|
+
strip_motd
|
377
|
+
@current_players = server_info["numplayers"].to_i
|
378
|
+
@max_players = server_info["maxplayers"].to_i
|
379
|
+
unless server_info["plugins"].nil? || server_info["plugins"].empty?
|
380
|
+
# Vanilla servers do not send a list of plugins.
|
381
|
+
# Bukkit and derivatives send plugins in the form: Paper on 1.19.3-R0.1-SNAPSHOT: Essentials 2.19.7; EssentialsChat 2.19.7
|
382
|
+
@plugin_list = server_info["plugins"].split(':')
|
383
|
+
@plugin_list = @plugin_list[1].split(';').collect(&:strip) if @plugin_list.size > 1
|
384
|
+
end
|
385
|
+
@online = true
|
386
|
+
else
|
387
|
+
return Retval::UNKNOWN
|
388
|
+
end
|
272
389
|
else # SLP
|
273
390
|
if server_info != nil && server_info.length >= NUM_FIELDS
|
274
391
|
# server_info[0] contains the section symbol and 1
|
@@ -285,18 +402,22 @@ class MineStat
|
|
285
402
|
end
|
286
403
|
return Retval::SUCCESS
|
287
404
|
end
|
288
|
-
|
289
|
-
|
290
|
-
# 1.
|
291
|
-
#
|
292
|
-
#
|
293
|
-
#
|
294
|
-
#
|
295
|
-
#
|
296
|
-
#
|
297
|
-
#
|
298
|
-
#
|
299
|
-
#
|
405
|
+
private :parse_data
|
406
|
+
|
407
|
+
# 1.8b - 1.3 (SLP request)
|
408
|
+
# @note
|
409
|
+
# 1. Client sends 0xFE (server list ping)
|
410
|
+
# 2. Server responds with:
|
411
|
+
# 2a. 0xFF (kick packet)
|
412
|
+
# 2b. data length
|
413
|
+
# 2c. 3 fields delimited by \u00A7 (section symbol)
|
414
|
+
# The 3 fields, in order, are:
|
415
|
+
# * message of the day
|
416
|
+
# * current players
|
417
|
+
# * max players
|
418
|
+
# @return [Retval] Return value
|
419
|
+
# @since 0.2.1
|
420
|
+
# @see https://wiki.vg/Server_List_Ping#Beta_1.8_to_1.3
|
300
421
|
def beta_request()
|
301
422
|
retval = nil
|
302
423
|
begin
|
@@ -310,7 +431,7 @@ class MineStat
|
|
310
431
|
rescue Timeout::Error
|
311
432
|
return Retval::TIMEOUT
|
312
433
|
rescue => exception
|
313
|
-
$stderr.puts exception
|
434
|
+
$stderr.puts exception if @debug
|
314
435
|
return Retval::UNKNOWN
|
315
436
|
end
|
316
437
|
if retval == Retval::SUCCESS
|
@@ -319,26 +440,29 @@ class MineStat
|
|
319
440
|
end
|
320
441
|
return retval
|
321
442
|
end
|
322
|
-
|
323
|
-
|
324
|
-
# 1.4 and 1.5
|
325
|
-
#
|
326
|
-
#
|
327
|
-
#
|
328
|
-
#
|
329
|
-
#
|
330
|
-
#
|
331
|
-
#
|
332
|
-
#
|
333
|
-
#
|
334
|
-
#
|
335
|
-
#
|
336
|
-
#
|
337
|
-
#
|
338
|
-
#
|
443
|
+
private :beta_request
|
444
|
+
|
445
|
+
# 1.4 and 1.5 (SLP request)
|
446
|
+
# @note
|
447
|
+
# 1. Client sends:
|
448
|
+
# 1a. 0xFE (server list ping)
|
449
|
+
# 1b. 0x01 (server list ping payload)
|
450
|
+
# 2. Server responds with:
|
451
|
+
# 2a. 0xFF (kick packet)
|
452
|
+
# 2b. data length
|
453
|
+
# 2c. 6 fields delimited by 0x00 (null)
|
454
|
+
# The 6 fields, in order, are:
|
455
|
+
# * the section symbol and 1
|
456
|
+
# * protocol version
|
457
|
+
# * server version
|
458
|
+
# * message of the day
|
459
|
+
# * current players
|
460
|
+
# * max players
|
339
461
|
#
|
340
|
-
#
|
341
|
-
#
|
462
|
+
# The protocol version corresponds with the server version and can be the
|
463
|
+
# same for different server versions.
|
464
|
+
# @return [Retval] Return value
|
465
|
+
# @see https://wiki.vg/Server_List_Ping#1.4_to_1.5
|
342
466
|
def legacy_request()
|
343
467
|
retval = nil
|
344
468
|
begin
|
@@ -352,7 +476,7 @@ class MineStat
|
|
352
476
|
rescue Timeout::Error
|
353
477
|
return Retval::TIMEOUT
|
354
478
|
rescue => exception
|
355
|
-
$stderr.puts exception
|
479
|
+
$stderr.puts exception if @debug
|
356
480
|
return Retval::UNKNOWN
|
357
481
|
end
|
358
482
|
if retval == Retval::SUCCESS
|
@@ -361,34 +485,38 @@ class MineStat
|
|
361
485
|
end
|
362
486
|
return retval
|
363
487
|
end
|
364
|
-
|
365
|
-
|
366
|
-
# 1.6
|
367
|
-
#
|
368
|
-
#
|
369
|
-
#
|
370
|
-
#
|
371
|
-
#
|
372
|
-
#
|
373
|
-
#
|
374
|
-
#
|
375
|
-
#
|
376
|
-
#
|
377
|
-
#
|
378
|
-
#
|
379
|
-
#
|
380
|
-
#
|
381
|
-
#
|
382
|
-
#
|
383
|
-
#
|
384
|
-
#
|
385
|
-
#
|
386
|
-
#
|
387
|
-
#
|
388
|
-
#
|
488
|
+
private :legacy_request
|
489
|
+
|
490
|
+
# 1.6 (SLP request)
|
491
|
+
# @note
|
492
|
+
# 1. Client sends:
|
493
|
+
# 1a. 0xFE (server list ping)
|
494
|
+
# 1b. 0x01 (server list ping payload)
|
495
|
+
# 1c. 0xFA (plugin message)
|
496
|
+
# 1d. 0x00 0x0B (11 which is the length of "MC|PingHost")
|
497
|
+
# 1e. "MC|PingHost" encoded as a UTF-16BE string
|
498
|
+
# 1f. length of remaining data as a short: remote address (encoded as UTF-16BE) + 7
|
499
|
+
# 1g. arbitrary 1.6 protocol version (0x4E for example for 78)
|
500
|
+
# 1h. length of remote address as a short
|
501
|
+
# 1i. remote address encoded as a UTF-16BE string
|
502
|
+
# 1j. remote port as an int
|
503
|
+
# 2. Server responds with:
|
504
|
+
# 2a. 0xFF (kick packet)
|
505
|
+
# 2b. data length
|
506
|
+
# 2c. 6 fields delimited by 0x00 (null)
|
507
|
+
# The 6 fields, in order, are:
|
508
|
+
# * the section symbol and 1
|
509
|
+
# * protocol version
|
510
|
+
# * server version
|
511
|
+
# * message of the day
|
512
|
+
# * current players
|
513
|
+
# * max players
|
389
514
|
#
|
390
515
|
# The protocol version corresponds with the server version and can be the
|
391
516
|
# same for different server versions.
|
517
|
+
# @return [Retval] Return value
|
518
|
+
# @since 0.2.0
|
519
|
+
# @see https://wiki.vg/Server_List_Ping#1.6
|
392
520
|
def extended_legacy_request()
|
393
521
|
retval = nil
|
394
522
|
begin
|
@@ -409,7 +537,7 @@ class MineStat
|
|
409
537
|
rescue Timeout::Error
|
410
538
|
return Retval::TIMEOUT
|
411
539
|
rescue => exception
|
412
|
-
$stderr.puts exception
|
540
|
+
$stderr.puts exception if @debug
|
413
541
|
return Retval::UNKNOWN
|
414
542
|
end
|
415
543
|
if retval == Retval::SUCCESS
|
@@ -418,23 +546,27 @@ class MineStat
|
|
418
546
|
end
|
419
547
|
return retval
|
420
548
|
end
|
421
|
-
|
422
|
-
|
423
|
-
# 1.7
|
424
|
-
#
|
425
|
-
#
|
426
|
-
#
|
427
|
-
#
|
428
|
-
#
|
429
|
-
#
|
430
|
-
#
|
431
|
-
#
|
432
|
-
#
|
433
|
-
#
|
434
|
-
#
|
435
|
-
#
|
436
|
-
#
|
437
|
-
#
|
549
|
+
private :extended_legacy_request
|
550
|
+
|
551
|
+
# >=1.7 (SLP request)
|
552
|
+
# @note
|
553
|
+
# 1. Client sends:
|
554
|
+
# 1a. 0x00 (handshake packet containing the fields specified below)
|
555
|
+
# 1b. 0x00 (request)
|
556
|
+
# The handshake packet contains the following fields respectively:
|
557
|
+
# 1. protocol version as a varint (0x00 suffices)
|
558
|
+
# 2. remote address as a string
|
559
|
+
# 3. remote port as an unsigned short
|
560
|
+
# 4. state as a varint (should be 1 for status)
|
561
|
+
# 2. Server responds with:
|
562
|
+
# 2a. 0x00 (JSON response)
|
563
|
+
# An example JSON string contains:
|
564
|
+
# {'players': {'max': 20, 'online': 0},
|
565
|
+
# 'version': {'protocol': 404, 'name': '1.13.2'},
|
566
|
+
# 'description': {'text': 'A Minecraft Server'}}
|
567
|
+
# @return [Retval] Return value
|
568
|
+
# @since 0.3.0
|
569
|
+
# @see https://wiki.vg/Server_List_Ping#Current_.281.7.2B.29
|
438
570
|
def json_request()
|
439
571
|
retval = nil
|
440
572
|
begin
|
@@ -483,7 +615,7 @@ class MineStat
|
|
483
615
|
rescue JSON::ParserError
|
484
616
|
return Retval::UNKNOWN
|
485
617
|
rescue => exception
|
486
|
-
$stderr.puts exception
|
618
|
+
$stderr.puts exception if @debug
|
487
619
|
return Retval::UNKNOWN
|
488
620
|
end
|
489
621
|
if retval == Retval::SUCCESS
|
@@ -492,8 +624,11 @@ class MineStat
|
|
492
624
|
end
|
493
625
|
return retval
|
494
626
|
end
|
627
|
+
private :json_request
|
495
628
|
|
496
629
|
# Reads JSON data from the socket
|
630
|
+
# @param json_len [Integer] Length of the JSON data received from the Minecraft server
|
631
|
+
# @return [String] JSON data received from the Mincraft server
|
497
632
|
def recv_json(json_len)
|
498
633
|
json_data = ""
|
499
634
|
begin
|
@@ -505,12 +640,15 @@ class MineStat
|
|
505
640
|
break if json_data.length >= json_len
|
506
641
|
end
|
507
642
|
rescue => exception
|
508
|
-
$stderr.puts exception
|
643
|
+
$stderr.puts exception if @debug
|
509
644
|
end
|
510
645
|
return json_data
|
511
646
|
end
|
647
|
+
private :recv_json
|
512
648
|
|
513
|
-
#
|
649
|
+
# Decodes the value of a varint type
|
650
|
+
# @return [Integer] Value decoded from a varint type
|
651
|
+
# @see https://en.wikipedia.org/wiki/LEB128
|
514
652
|
def unpack_varint()
|
515
653
|
vint = 0
|
516
654
|
i = 0
|
@@ -524,34 +662,38 @@ class MineStat
|
|
524
662
|
end
|
525
663
|
return vint
|
526
664
|
end
|
527
|
-
|
528
|
-
|
529
|
-
# Bedrock/Pocket Edition
|
530
|
-
#
|
531
|
-
#
|
532
|
-
#
|
533
|
-
#
|
534
|
-
#
|
535
|
-
#
|
536
|
-
#
|
537
|
-
#
|
538
|
-
#
|
539
|
-
#
|
540
|
-
#
|
541
|
-
#
|
542
|
-
#
|
543
|
-
#
|
544
|
-
#
|
545
|
-
#
|
546
|
-
#
|
547
|
-
#
|
548
|
-
#
|
549
|
-
#
|
550
|
-
#
|
551
|
-
#
|
552
|
-
#
|
553
|
-
#
|
554
|
-
#
|
665
|
+
private :unpack_varint
|
666
|
+
|
667
|
+
# Bedrock/Pocket Edition (unconnected ping request)
|
668
|
+
# @note
|
669
|
+
# 1. Client sends:
|
670
|
+
# 1a. 0x01 (unconnected ping packet containing the fields specified below)
|
671
|
+
# 1b. current time as a long
|
672
|
+
# 1c. magic number
|
673
|
+
# 1d. client GUID as a long
|
674
|
+
# 2. Server responds with:
|
675
|
+
# 2a. 0x1c (unconnected pong packet containing the follow fields)
|
676
|
+
# 2b. current time as a long
|
677
|
+
# 2c. server GUID as a long
|
678
|
+
# 2d. 16-bit magic number
|
679
|
+
# 2e. server ID string length
|
680
|
+
# 2f. server ID as a string
|
681
|
+
# The fields from the pong response, in order, are:
|
682
|
+
# * edition
|
683
|
+
# * MotD line 1
|
684
|
+
# * protocol version
|
685
|
+
# * version name
|
686
|
+
# * current player count
|
687
|
+
# * maximum player count
|
688
|
+
# * unique server ID
|
689
|
+
# * MotD line 2
|
690
|
+
# * game mode as a string
|
691
|
+
# * game mode as a numeric
|
692
|
+
# * IPv4 port number
|
693
|
+
# * IPv6 port number
|
694
|
+
# @return [Retval] Return value
|
695
|
+
# @since 2.2.0
|
696
|
+
# @see https://wiki.vg/Raknet_Protocol#Unconnected_Ping
|
555
697
|
def bedrock_request()
|
556
698
|
retval = nil
|
557
699
|
begin
|
@@ -571,7 +713,73 @@ class MineStat
|
|
571
713
|
rescue Timeout::Error
|
572
714
|
return Retval::TIMEOUT
|
573
715
|
rescue => exception
|
574
|
-
$stderr.puts exception
|
716
|
+
$stderr.puts exception if @debug
|
717
|
+
return Retval::UNKNOWN
|
718
|
+
end
|
719
|
+
if retval == Retval::SUCCESS
|
720
|
+
set_connection_status(retval)
|
721
|
+
end
|
722
|
+
return retval
|
723
|
+
end
|
724
|
+
private :bedrock_request
|
725
|
+
|
726
|
+
# Unreal Tournament 3/GameSpy 4 (UT3/GS4) query protocol
|
727
|
+
# @note
|
728
|
+
# 1. Client sends:
|
729
|
+
# 1a. 0xFE 0xFD (query identifier)
|
730
|
+
# 1b. 0x09 (handshake)
|
731
|
+
# 1c. arbitrary session ID (4 bytes)
|
732
|
+
# 2. Server responds with:
|
733
|
+
# 2a. 0x09 (handshake)
|
734
|
+
# 2b. session ID (4 bytes)
|
735
|
+
# 2c. challenge token (variable null-terminated string)
|
736
|
+
# 3. Client sends:
|
737
|
+
# 3a. 0xFE 0xFD (query identifier)
|
738
|
+
# 3b. 0x00 (stat)
|
739
|
+
# 3c. arbitrary session ID (4 bytes)
|
740
|
+
# 3d. challenge token (32-bit integer in network byte order)
|
741
|
+
# 3e. padding (4 bytes -- 0x00 0x00 0x00 0x00); omit padding for basic stat (which does not supply the version)
|
742
|
+
# 4. Server responds with:
|
743
|
+
# 4a. 0x00 (stat)
|
744
|
+
# 4b. session ID (4 bytes)
|
745
|
+
# 4c. padding (11 bytes)
|
746
|
+
# 4e. key/value pairs of multiple null-terminated strings containing the fields below:
|
747
|
+
# hostname, game type, game ID, version, plugin list, map, current players, max players, port, address
|
748
|
+
# 4f. padding (10 bytes)
|
749
|
+
# 4g. list of null-terminated strings containing player names
|
750
|
+
# @return [Retval] Return value
|
751
|
+
# @since 3.0.0
|
752
|
+
# @see https://wiki.vg/Query
|
753
|
+
def query_request()
|
754
|
+
retval = nil
|
755
|
+
begin
|
756
|
+
Timeout::timeout(@timeout) do
|
757
|
+
@request_type = "UT3/GS4 Query"
|
758
|
+
retval = connect()
|
759
|
+
return retval unless retval == Retval::SUCCESS
|
760
|
+
payload = "\xFE\xFD\x09\x0B\x03\x03\x0F"
|
761
|
+
@server.write(payload)
|
762
|
+
@server.flush
|
763
|
+
if @server.recv(1, Socket::MSG_PEEK).unpack('C').first == 0x09 # query handshake packet
|
764
|
+
# Session ID generated by the server is not used -- use a static session ID instead such as 0x0B 0x03 0x03 0x0F.
|
765
|
+
#session_id = @server.recv(QUERY_HANDSHAKE_OFFSET, Socket::MSG_PEEK)[1..-1].unpack('l>')
|
766
|
+
challenge_token = @server.recv(QUERY_HANDSHAKE_SIZE)[QUERY_HANDSHAKE_OFFSET..-1]
|
767
|
+
payload = "\xFE\xFD\x00\x0B\x03\x03\x0F".force_encoding('ASCII-8BIT')
|
768
|
+
# Use the full stat below by stripping the null terminator from the challenge token and padding the end
|
769
|
+
# of the payload with "\x00\x00\x00\x00". The basic stat response does not include the server version.
|
770
|
+
payload += [challenge_token.rstrip.to_i].pack('l>').force_encoding('ASCII-8BIT')
|
771
|
+
payload += "\x00\x00\x00\x00".force_encoding('ASCII-8BIT')
|
772
|
+
@server.write(payload)
|
773
|
+
@server.flush
|
774
|
+
else
|
775
|
+
return Retval::UNKNOWN
|
776
|
+
end
|
777
|
+
retval = parse_data("\x00") # null
|
778
|
+
end
|
779
|
+
rescue Timeout::Error
|
780
|
+
return Retval::TIMEOUT
|
781
|
+
rescue => exception
|
782
|
+
$stderr.puts exception if @debug
|
575
783
|
return Retval::UNKNOWN
|
576
784
|
end
|
577
785
|
if retval == Retval::SUCCESS
|
@@ -579,62 +787,89 @@ class MineStat
|
|
579
787
|
end
|
580
788
|
return retval
|
581
789
|
end
|
790
|
+
private :query_request
|
582
791
|
|
583
|
-
#
|
792
|
+
# Address (hostname or IP address) of the Minecraft server
|
584
793
|
attr_reader :address
|
585
794
|
|
586
|
-
#
|
795
|
+
# Port (TCP or UDP) of the Minecraft server
|
587
796
|
attr_reader :port
|
588
797
|
|
589
|
-
#
|
798
|
+
# Whether or not the Minecraft server is online
|
590
799
|
attr_reader :online
|
591
800
|
|
592
|
-
#
|
801
|
+
# Minecraft server version
|
593
802
|
attr_reader :version
|
594
803
|
|
595
|
-
#
|
804
|
+
# Game mode
|
805
|
+
# @note Bedrock/Pocket Edition only
|
806
|
+
# @since 2.2.0
|
596
807
|
attr_reader :mode
|
597
808
|
|
598
|
-
#
|
599
|
-
#
|
600
|
-
#
|
809
|
+
# Full message of the day (MotD)
|
810
|
+
# @note If only the plain text MotD is relevant, use {#stripped_motd}
|
811
|
+
# @see #stripped_motd
|
601
812
|
attr_reader :motd
|
602
813
|
|
603
|
-
#
|
814
|
+
# Plain text contained within the message of the day (MotD)
|
815
|
+
# @note If the full MotD is desired, use {#motd}
|
816
|
+
# @see #motd
|
604
817
|
attr_reader :stripped_motd
|
605
818
|
|
606
|
-
#
|
819
|
+
# Current player count
|
607
820
|
attr_reader :current_players
|
608
821
|
|
609
|
-
#
|
822
|
+
# Maximum player limit
|
610
823
|
attr_reader :max_players
|
611
824
|
|
612
|
-
#
|
613
|
-
#
|
614
|
-
#
|
615
|
-
|
616
|
-
|
825
|
+
# List of players
|
826
|
+
# @note UT3/GS4 query only
|
827
|
+
# @since 3.0.0
|
828
|
+
attr_reader :player_list
|
829
|
+
|
830
|
+
# List of plugins
|
831
|
+
# @note UT3/GS4 query only
|
832
|
+
# @since 3.0.0
|
833
|
+
attr_reader :plugin_list
|
834
|
+
|
835
|
+
# Protocol level
|
836
|
+
# @note This is arbitrary and varies by Minecraft version (may also be shared by multiple Minecraft versions)
|
617
837
|
attr_reader :protocol
|
618
838
|
|
619
|
-
#
|
620
|
-
#
|
839
|
+
# Complete JSON response data
|
840
|
+
# @note Received using SLP 1.7 (JSON) queries
|
841
|
+
# @since 0.3.0
|
621
842
|
attr_reader :json_data
|
622
843
|
|
623
|
-
#
|
844
|
+
# Base64-encoded favicon
|
845
|
+
# @note Received using SLP 1.7 (JSON) queries
|
846
|
+
# @since 2.2.2
|
624
847
|
attr_reader :favicon_b64
|
625
848
|
|
626
|
-
#
|
849
|
+
# Decoded favicon
|
850
|
+
# @note Received using SLP 1.7 (JSON) queries
|
851
|
+
# @since 2.2.2
|
627
852
|
attr_reader :favicon
|
628
853
|
|
629
|
-
#
|
854
|
+
# Ping time to the server in milliseconds (ms)
|
855
|
+
# @since 0.2.1
|
630
856
|
attr_reader :latency
|
631
857
|
|
632
|
-
#
|
858
|
+
# TCP/UDP timeout in seconds
|
859
|
+
# @since 0.1.2
|
860
|
+
attr_accessor :timeout
|
861
|
+
|
862
|
+
# Protocol used to request data from a Minecraft server
|
633
863
|
attr_reader :request_type
|
634
864
|
|
635
|
-
#
|
865
|
+
# Connection status
|
866
|
+
# @since 2.2.2
|
636
867
|
attr_reader :connection_status
|
637
868
|
|
638
|
-
#
|
869
|
+
# Whether or not all protocols should be attempted
|
639
870
|
attr_reader :try_all
|
871
|
+
|
872
|
+
# Whether or not debug mode is enabled
|
873
|
+
# @since 3.0.0
|
874
|
+
attr_reader :debug
|
640
875
|
end
|