minestat 2.3.0 → 3.0.0

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