minestat 2.2.4 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +425 -171
- metadata +16 -8
data/lib/minestat.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# minestat.rb - A Minecraft server status checker
|
2
|
-
# Copyright (C) 2014-
|
2
|
+
# Copyright (C) 2014-2023 Lloyd Dilley
|
3
3
|
# http://www.dilley.me/
|
4
4
|
#
|
5
5
|
# This program is free software; you can redistribute it and/or modify
|
@@ -18,78 +18,135 @@
|
|
18
18
|
|
19
19
|
require 'base64'
|
20
20
|
require 'json'
|
21
|
+
require 'resolv'
|
21
22
|
require 'socket'
|
22
23
|
require 'timeout'
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
# @author Lloyd Dilley
|
26
|
+
|
27
|
+
# Provides a Ruby interface for polling the status of Minecraft servers
|
26
28
|
class MineStat
|
27
29
|
# MineStat version
|
28
|
-
VERSION = "
|
30
|
+
VERSION = "3.0.0"
|
31
|
+
|
29
32
|
# Number of values expected from server
|
30
33
|
NUM_FIELDS = 6
|
31
|
-
|
34
|
+
private_constant :NUM_FIELDS
|
35
|
+
|
36
|
+
# Number of values expected from a 1.8b - 1.3 server
|
32
37
|
NUM_FIELDS_BETA = 3
|
38
|
+
private_constant :NUM_FIELDS_BETA
|
39
|
+
|
33
40
|
# Maximum number of bytes a varint can be
|
34
41
|
MAX_VARINT_SIZE = 5
|
42
|
+
private_constant :MAX_VARINT_SIZE
|
43
|
+
|
35
44
|
# Default TCP port
|
36
45
|
DEFAULT_TCP_PORT = 25565
|
46
|
+
|
37
47
|
# Bedrock/Pocket Edition default UDP port
|
38
48
|
DEFAULT_BEDROCK_PORT = 19132
|
49
|
+
|
39
50
|
# Default TCP/UDP timeout in seconds
|
40
51
|
DEFAULT_TIMEOUT = 5
|
52
|
+
|
41
53
|
# Bedrock/Pocket Edition packet offset in bytes (1 + 8 + 8 + 16 + 2)
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
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
|
47
59
|
BEDROCK_PACKET_OFFSET = 35
|
48
|
-
|
49
|
-
|
50
|
-
#
|
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
|
51
83
|
module Retval
|
52
84
|
# The server ping completed successfully
|
53
85
|
SUCCESS = 0
|
86
|
+
|
54
87
|
# The server ping failed due to a connection error
|
55
88
|
CONNFAIL = -1
|
89
|
+
|
56
90
|
# The server ping failed due to a connection time out
|
57
91
|
TIMEOUT = -2
|
92
|
+
|
58
93
|
# The server ping failed for an unknown reason
|
59
94
|
UNKNOWN = -3
|
60
95
|
end
|
61
96
|
|
62
|
-
|
63
|
-
# Stores constants that represent the different kinds of server
|
64
|
-
# list pings/requests that a Minecraft server might expect when
|
65
|
-
# being polled for status information.
|
97
|
+
# These constants represent the various protocols used when requesting server data
|
66
98
|
module Request
|
67
99
|
# Try everything
|
68
100
|
NONE = -1
|
101
|
+
|
69
102
|
# Server versions 1.8b to 1.3
|
70
103
|
BETA = 0
|
104
|
+
|
71
105
|
# Server versions 1.4 to 1.5
|
72
106
|
LEGACY = 1
|
107
|
+
|
73
108
|
# Server version 1.6
|
74
109
|
EXTENDED = 2
|
110
|
+
|
75
111
|
# Server versions 1.7 to latest
|
76
112
|
JSON = 3
|
113
|
+
|
77
114
|
# Bedrock/Pocket Edition
|
78
115
|
BEDROCK = 4
|
116
|
+
|
117
|
+
# Unreal Tournament 3/GameSpy 4 query
|
118
|
+
QUERY = 5
|
79
119
|
end
|
80
120
|
|
81
|
-
|
82
|
-
#
|
83
|
-
|
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)
|
84
139
|
@address = address # address of server
|
85
140
|
@port = port # TCP/UDP port of server
|
86
141
|
@online # online or offline?
|
87
142
|
@version # server version
|
88
|
-
@mode
|
143
|
+
@mode # game mode (Bedrock/Pocket Edition only)
|
89
144
|
@motd # message of the day
|
90
145
|
@stripped_motd # message of the day without formatting
|
91
146
|
@current_players # current number of players online
|
92
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)
|
93
150
|
@protocol # protocol level
|
94
151
|
@json_data # JSON data for 1.7 queries
|
95
152
|
@favicon_b64 # base64-encoded favicon possibly contained in JSON 1.7 responses
|
@@ -100,9 +157,38 @@ class MineStat
|
|
100
157
|
@request_type # protocol version
|
101
158
|
@connection_status # status of connection ("Success", "Fail", "Timeout", or "Unknown")
|
102
159
|
@try_all = false # try all protocols?
|
160
|
+
@debug = debug # debug mode
|
103
161
|
|
104
162
|
@try_all = true if request_type == Request::NONE
|
163
|
+
resolve_srv(address, port)
|
164
|
+
set_connection_status(attempt_protocols(request_type))
|
165
|
+
end
|
166
|
+
|
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
|
172
|
+
def resolve_srv(address, port)
|
173
|
+
begin
|
174
|
+
resolver = Resolv::DNS.new
|
175
|
+
res = resolver.getresource("_minecraft._tcp.#{@address}", Resolv::DNS::Resource::IN::SRV)
|
176
|
+
@address = res.target.to_s # SRV target
|
177
|
+
@port = res.port.to_i # SRV port
|
178
|
+
rescue => exception # primarily catch Resolv::ResolvError and revert if unable to resolve SRV record(s)
|
179
|
+
$stderr.puts exception if @debug
|
180
|
+
@address = address
|
181
|
+
@port = port
|
182
|
+
return false
|
183
|
+
end
|
184
|
+
return true
|
185
|
+
end
|
186
|
+
private :resolve_srv
|
105
187
|
|
188
|
+
# Attempts the use of various protocols
|
189
|
+
# @param request_type [Request] Protocol used to poll a Minecraft server
|
190
|
+
# @return [Retval] Return value
|
191
|
+
def attempt_protocols(request_type)
|
106
192
|
case request_type
|
107
193
|
when Request::BETA
|
108
194
|
retval = beta_request()
|
@@ -114,6 +200,8 @@ class MineStat
|
|
114
200
|
retval = json_request()
|
115
201
|
when Request::BEDROCK
|
116
202
|
retval = bedrock_request()
|
203
|
+
when Request::QUERY
|
204
|
+
retval = query_request()
|
117
205
|
else
|
118
206
|
# Attempt various ping requests in a particular order. If the
|
119
207
|
# connection fails, there is no reason to continue with subsequent
|
@@ -138,17 +226,24 @@ class MineStat
|
|
138
226
|
unless @online || retval == Retval::SUCCESS
|
139
227
|
retval = bedrock_request()
|
140
228
|
end
|
229
|
+
# UT3/GS4 query
|
230
|
+
unless @online || retval == Retval::SUCCESS
|
231
|
+
retval = query_request()
|
232
|
+
end
|
141
233
|
end
|
142
|
-
|
234
|
+
return retval
|
143
235
|
end
|
236
|
+
private :attempt_protocols
|
144
237
|
|
145
238
|
# Sets connection status
|
239
|
+
# @param retval [Retval] Return value
|
146
240
|
def set_connection_status(retval)
|
147
|
-
@connection_status = "Success" if retval == Retval::SUCCESS
|
241
|
+
@connection_status = "Success" if @online || retval == Retval::SUCCESS
|
148
242
|
@connection_status = "Fail" if retval == Retval::CONNFAIL
|
149
243
|
@connection_status = "Timeout" if retval == Retval::TIMEOUT
|
150
244
|
@connection_status = "Unknown" if retval == Retval::UNKNOWN
|
151
245
|
end
|
246
|
+
private :set_connection_status
|
152
247
|
|
153
248
|
# Strips message of the day formatting characters
|
154
249
|
def strip_motd()
|
@@ -168,13 +263,13 @@ class MineStat
|
|
168
263
|
@stripped_motd = @stripped_motd.force_encoding('UTF-8')
|
169
264
|
@stripped_motd = @stripped_motd.gsub(/§./, "")
|
170
265
|
end
|
266
|
+
private :strip_motd
|
171
267
|
|
172
|
-
##
|
173
268
|
# Establishes a connection to the Minecraft server
|
174
269
|
def connect()
|
175
270
|
begin
|
176
|
-
if @request_type == Request::BEDROCK || @request_type == "Bedrock/Pocket Edition"
|
177
|
-
@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
|
178
273
|
start_time = Time.now
|
179
274
|
@server = UDPSocket.new
|
180
275
|
@server.connect(@address, @port)
|
@@ -186,15 +281,18 @@ class MineStat
|
|
186
281
|
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
187
282
|
return Retval::CONNFAIL
|
188
283
|
rescue => exception
|
189
|
-
$stderr.puts exception
|
284
|
+
$stderr.puts exception if @debug
|
190
285
|
return Retval::UNKNOWN
|
191
286
|
end
|
192
287
|
return Retval::SUCCESS
|
193
288
|
end
|
289
|
+
private :connect
|
194
290
|
|
195
|
-
#
|
196
|
-
|
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()
|
197
294
|
data = nil
|
295
|
+
retval = nil
|
198
296
|
begin
|
199
297
|
if @request_type == "Bedrock/Pocket Edition"
|
200
298
|
if @server.recv(1, Socket::MSG_PEEK).unpack('C').first == 0x1C # unconnected pong packet
|
@@ -203,7 +301,15 @@ class MineStat
|
|
203
301
|
@server.close
|
204
302
|
else
|
205
303
|
@server.close
|
206
|
-
|
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
|
207
313
|
end
|
208
314
|
else # SLP
|
209
315
|
if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
|
@@ -212,20 +318,31 @@ class MineStat
|
|
212
318
|
@server.close
|
213
319
|
else
|
214
320
|
@server.close
|
215
|
-
|
321
|
+
retval = Retval::UNKNOWN
|
216
322
|
end
|
217
323
|
end
|
218
|
-
rescue
|
219
|
-
|
220
|
-
|
221
|
-
return Retval::UNKNOWN
|
324
|
+
rescue => exception
|
325
|
+
$stderr.puts exception if @debug
|
326
|
+
return nil, Retval::UNKNOWN
|
222
327
|
end
|
328
|
+
retval = Retval::UNKNOWN if data == nil || data.empty?
|
329
|
+
return data, retval
|
330
|
+
end
|
331
|
+
private :check_response
|
223
332
|
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
227
339
|
|
228
|
-
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
|
229
346
|
if is_beta
|
230
347
|
if server_info != nil && server_info.length >= NUM_FIELDS_BETA
|
231
348
|
@version = ">=1.8b/1.3" # since server does not return version, set it
|
@@ -250,6 +367,25 @@ class MineStat
|
|
250
367
|
else
|
251
368
|
return Retval::UNKNOWN
|
252
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
|
253
389
|
else # SLP
|
254
390
|
if server_info != nil && server_info.length >= NUM_FIELDS
|
255
391
|
# server_info[0] contains the section symbol and 1
|
@@ -266,18 +402,22 @@ class MineStat
|
|
266
402
|
end
|
267
403
|
return Retval::SUCCESS
|
268
404
|
end
|
269
|
-
|
270
|
-
|
271
|
-
# 1.
|
272
|
-
#
|
273
|
-
#
|
274
|
-
#
|
275
|
-
#
|
276
|
-
#
|
277
|
-
#
|
278
|
-
#
|
279
|
-
#
|
280
|
-
#
|
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
|
281
421
|
def beta_request()
|
282
422
|
retval = nil
|
283
423
|
begin
|
@@ -291,7 +431,7 @@ class MineStat
|
|
291
431
|
rescue Timeout::Error
|
292
432
|
return Retval::TIMEOUT
|
293
433
|
rescue => exception
|
294
|
-
$stderr.puts exception
|
434
|
+
$stderr.puts exception if @debug
|
295
435
|
return Retval::UNKNOWN
|
296
436
|
end
|
297
437
|
if retval == Retval::SUCCESS
|
@@ -300,26 +440,29 @@ class MineStat
|
|
300
440
|
end
|
301
441
|
return retval
|
302
442
|
end
|
303
|
-
|
304
|
-
|
305
|
-
# 1.4 and 1.5
|
306
|
-
#
|
307
|
-
#
|
308
|
-
#
|
309
|
-
#
|
310
|
-
#
|
311
|
-
#
|
312
|
-
#
|
313
|
-
#
|
314
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
#
|
318
|
-
#
|
319
|
-
#
|
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
|
320
461
|
#
|
321
|
-
#
|
322
|
-
#
|
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
|
323
466
|
def legacy_request()
|
324
467
|
retval = nil
|
325
468
|
begin
|
@@ -333,7 +476,7 @@ class MineStat
|
|
333
476
|
rescue Timeout::Error
|
334
477
|
return Retval::TIMEOUT
|
335
478
|
rescue => exception
|
336
|
-
$stderr.puts exception
|
479
|
+
$stderr.puts exception if @debug
|
337
480
|
return Retval::UNKNOWN
|
338
481
|
end
|
339
482
|
if retval == Retval::SUCCESS
|
@@ -342,34 +485,38 @@ class MineStat
|
|
342
485
|
end
|
343
486
|
return retval
|
344
487
|
end
|
345
|
-
|
346
|
-
|
347
|
-
# 1.6
|
348
|
-
#
|
349
|
-
#
|
350
|
-
#
|
351
|
-
#
|
352
|
-
#
|
353
|
-
#
|
354
|
-
#
|
355
|
-
#
|
356
|
-
#
|
357
|
-
#
|
358
|
-
#
|
359
|
-
#
|
360
|
-
#
|
361
|
-
#
|
362
|
-
#
|
363
|
-
#
|
364
|
-
#
|
365
|
-
#
|
366
|
-
#
|
367
|
-
#
|
368
|
-
#
|
369
|
-
#
|
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
|
370
514
|
#
|
371
515
|
# The protocol version corresponds with the server version and can be the
|
372
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
|
373
520
|
def extended_legacy_request()
|
374
521
|
retval = nil
|
375
522
|
begin
|
@@ -390,7 +537,7 @@ class MineStat
|
|
390
537
|
rescue Timeout::Error
|
391
538
|
return Retval::TIMEOUT
|
392
539
|
rescue => exception
|
393
|
-
$stderr.puts exception
|
540
|
+
$stderr.puts exception if @debug
|
394
541
|
return Retval::UNKNOWN
|
395
542
|
end
|
396
543
|
if retval == Retval::SUCCESS
|
@@ -399,23 +546,27 @@ class MineStat
|
|
399
546
|
end
|
400
547
|
return retval
|
401
548
|
end
|
402
|
-
|
403
|
-
|
404
|
-
# 1.7
|
405
|
-
#
|
406
|
-
#
|
407
|
-
#
|
408
|
-
#
|
409
|
-
#
|
410
|
-
#
|
411
|
-
#
|
412
|
-
#
|
413
|
-
#
|
414
|
-
#
|
415
|
-
#
|
416
|
-
#
|
417
|
-
#
|
418
|
-
#
|
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
|
419
570
|
def json_request()
|
420
571
|
retval = nil
|
421
572
|
begin
|
@@ -464,7 +615,7 @@ class MineStat
|
|
464
615
|
rescue JSON::ParserError
|
465
616
|
return Retval::UNKNOWN
|
466
617
|
rescue => exception
|
467
|
-
$stderr.puts exception
|
618
|
+
$stderr.puts exception if @debug
|
468
619
|
return Retval::UNKNOWN
|
469
620
|
end
|
470
621
|
if retval == Retval::SUCCESS
|
@@ -473,8 +624,11 @@ class MineStat
|
|
473
624
|
end
|
474
625
|
return retval
|
475
626
|
end
|
627
|
+
private :json_request
|
476
628
|
|
477
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
|
478
632
|
def recv_json(json_len)
|
479
633
|
json_data = ""
|
480
634
|
begin
|
@@ -486,12 +640,15 @@ class MineStat
|
|
486
640
|
break if json_data.length >= json_len
|
487
641
|
end
|
488
642
|
rescue => exception
|
489
|
-
$stderr.puts exception
|
643
|
+
$stderr.puts exception if @debug
|
490
644
|
end
|
491
645
|
return json_data
|
492
646
|
end
|
647
|
+
private :recv_json
|
493
648
|
|
494
|
-
#
|
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
|
495
652
|
def unpack_varint()
|
496
653
|
vint = 0
|
497
654
|
i = 0
|
@@ -505,34 +662,38 @@ class MineStat
|
|
505
662
|
end
|
506
663
|
return vint
|
507
664
|
end
|
508
|
-
|
509
|
-
|
510
|
-
# Bedrock/Pocket Edition
|
511
|
-
#
|
512
|
-
#
|
513
|
-
#
|
514
|
-
#
|
515
|
-
#
|
516
|
-
#
|
517
|
-
#
|
518
|
-
#
|
519
|
-
#
|
520
|
-
#
|
521
|
-
#
|
522
|
-
#
|
523
|
-
#
|
524
|
-
#
|
525
|
-
#
|
526
|
-
#
|
527
|
-
#
|
528
|
-
#
|
529
|
-
#
|
530
|
-
#
|
531
|
-
#
|
532
|
-
#
|
533
|
-
#
|
534
|
-
#
|
535
|
-
#
|
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
|
536
697
|
def bedrock_request()
|
537
698
|
retval = nil
|
538
699
|
begin
|
@@ -552,7 +713,7 @@ class MineStat
|
|
552
713
|
rescue Timeout::Error
|
553
714
|
return Retval::TIMEOUT
|
554
715
|
rescue => exception
|
555
|
-
$stderr.puts exception
|
716
|
+
$stderr.puts exception if @debug
|
556
717
|
return Retval::UNKNOWN
|
557
718
|
end
|
558
719
|
if retval == Retval::SUCCESS
|
@@ -560,62 +721,155 @@ class MineStat
|
|
560
721
|
end
|
561
722
|
return retval
|
562
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
|
783
|
+
return Retval::UNKNOWN
|
784
|
+
end
|
785
|
+
if retval == Retval::SUCCESS
|
786
|
+
set_connection_status(retval)
|
787
|
+
end
|
788
|
+
return retval
|
789
|
+
end
|
790
|
+
private :query_request
|
563
791
|
|
564
|
-
#
|
792
|
+
# Address (hostname or IP address) of the Minecraft server
|
565
793
|
attr_reader :address
|
566
794
|
|
567
|
-
#
|
795
|
+
# Port (TCP or UDP) of the Minecraft server
|
568
796
|
attr_reader :port
|
569
797
|
|
570
|
-
#
|
798
|
+
# Whether or not the Minecraft server is online
|
571
799
|
attr_reader :online
|
572
800
|
|
573
|
-
#
|
801
|
+
# Minecraft server version
|
574
802
|
attr_reader :version
|
575
803
|
|
576
|
-
#
|
804
|
+
# Game mode
|
805
|
+
# @note Bedrock/Pocket Edition only
|
806
|
+
# @since 2.2.0
|
577
807
|
attr_reader :mode
|
578
808
|
|
579
|
-
#
|
580
|
-
#
|
581
|
-
#
|
809
|
+
# Full message of the day (MotD)
|
810
|
+
# @note If only the plain text MotD is relevant, use {#stripped_motd}
|
811
|
+
# @see #stripped_motd
|
582
812
|
attr_reader :motd
|
583
813
|
|
584
|
-
#
|
814
|
+
# Plain text contained within the message of the day (MotD)
|
815
|
+
# @note If the full MotD is desired, use {#motd}
|
816
|
+
# @see #motd
|
585
817
|
attr_reader :stripped_motd
|
586
818
|
|
587
|
-
#
|
819
|
+
# Current player count
|
588
820
|
attr_reader :current_players
|
589
821
|
|
590
|
-
#
|
822
|
+
# Maximum player limit
|
591
823
|
attr_reader :max_players
|
592
824
|
|
593
|
-
#
|
594
|
-
#
|
595
|
-
#
|
596
|
-
|
597
|
-
|
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)
|
598
837
|
attr_reader :protocol
|
599
838
|
|
600
|
-
#
|
601
|
-
#
|
839
|
+
# Complete JSON response data
|
840
|
+
# @note Received using SLP 1.7 (JSON) queries
|
841
|
+
# @since 0.3.0
|
602
842
|
attr_reader :json_data
|
603
843
|
|
604
|
-
#
|
844
|
+
# Base64-encoded favicon
|
845
|
+
# @note Received using SLP 1.7 (JSON) queries
|
846
|
+
# @since 2.2.2
|
605
847
|
attr_reader :favicon_b64
|
606
848
|
|
607
|
-
#
|
849
|
+
# Decoded favicon
|
850
|
+
# @note Received using SLP 1.7 (JSON) queries
|
851
|
+
# @since 2.2.2
|
608
852
|
attr_reader :favicon
|
609
853
|
|
610
|
-
#
|
854
|
+
# Ping time to the server in milliseconds (ms)
|
855
|
+
# @since 0.2.1
|
611
856
|
attr_reader :latency
|
612
857
|
|
613
|
-
#
|
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
|
614
863
|
attr_reader :request_type
|
615
864
|
|
616
|
-
#
|
865
|
+
# Connection status
|
866
|
+
# @since 2.2.2
|
617
867
|
attr_reader :connection_status
|
618
868
|
|
619
|
-
#
|
869
|
+
# Whether or not all protocols should be attempted
|
620
870
|
attr_reader :try_all
|
871
|
+
|
872
|
+
# Whether or not debug mode is enabled
|
873
|
+
# @since 3.0.0
|
874
|
+
attr_reader :debug
|
621
875
|
end
|