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