minestat 2.2.4 → 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 +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
|