minestat 2.2.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 +425 -171
  8. 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-2022 Lloyd Dilley
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
- # 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
26
28
  class MineStat
27
29
  # MineStat version
28
- VERSION = "2.2.4"
30
+ VERSION = "3.0.0"
31
+
29
32
  # Number of values expected from server
30
33
  NUM_FIELDS = 6
31
- # 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
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
- # Unconnected pong (0x1C) = 1 byte
43
- # Timestamp as a long = 8 bytes
44
- # Server GUID as a long = 8 bytes
45
- # Magic number = 16 bytes
46
- # 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
47
59
  BEDROCK_PACKET_OFFSET = 35
48
-
49
- ##
50
- # 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
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
- # Instantiate an instance of MineStat and poll the specified server for information
83
- 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)
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 = "Unspecified" # game mode (Bedrock/Pocket Edition only)
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
- set_connection_status(retval) unless @online
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
- # Populates object fields after connecting
196
- 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()
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
- 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
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
- return Retval::UNKNOWN
321
+ retval = Retval::UNKNOWN
216
322
  end
217
323
  end
218
- rescue
219
- #rescue => exception
220
- #$stderr.puts exception
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
- if data == nil || data.empty?
225
- return Retval::UNKNOWN
226
- 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
227
339
 
228
- 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
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.8 beta through 1.3 servers communicate as follows for a ping request:
272
- # 1. Client sends \xFE (server list ping)
273
- # 2. Server responds with:
274
- # 2a. \xFF (kick packet)
275
- # 2b. data length
276
- # 2c. 3 fields delimited by \u00A7 (section symbol)
277
- # The 3 fields, in order, are:
278
- # * message of the day
279
- # * current players
280
- # * 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
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 servers communicate as follows for a ping request:
306
- # 1. Client sends:
307
- # 1a. \xFE (server list ping)
308
- # 1b. \x01 (server list ping payload)
309
- # 2. Server responds with:
310
- # 2a. \xFF (kick packet)
311
- # 2b. data length
312
- # 2c. 6 fields delimited by \x00 (null)
313
- # The 6 fields, in order, are:
314
- # * the section symbol and 1
315
- # * protocol version
316
- # * server version
317
- # * message of the day
318
- # * current players
319
- # * 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
320
461
  #
321
- # The protocol version corresponds with the server version and can be the
322
- # 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
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 servers communicate as follows for a ping request:
348
- # 1. Client sends:
349
- # 1a. \xFE (server list ping)
350
- # 1b. \x01 (server list ping payload)
351
- # 1c. \xFA (plugin message)
352
- # 1d. \x00\x0B (11 which is the length of "MC|PingHost")
353
- # 1e. "MC|PingHost" encoded as a UTF-16BE string
354
- # 1f. length of remaining data as a short: remote address (encoded as UTF-16BE) + 7
355
- # 1g. arbitrary 1.6 protocol version (\x4E for example for 78)
356
- # 1h. length of remote address as a short
357
- # 1i. remote address encoded as a UTF-16BE string
358
- # 1j. remote port as an int
359
- # 2. Server responds with:
360
- # 2a. \xFF (kick packet)
361
- # 2b. data length
362
- # 2c. 6 fields delimited by \x00 (null)
363
- # The 6 fields, in order, are:
364
- # * the section symbol and 1
365
- # * protocol version
366
- # * server version
367
- # * message of the day
368
- # * current players
369
- # * 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
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 to current servers communicate as follows for a ping request:
405
- # 1. Client sends:
406
- # 1a. \x00 (handshake packet containing the fields specified below)
407
- # 1b. \x00 (request)
408
- # The handshake packet contains the following fields respectively:
409
- # 1. protocol version as a varint (\x00 suffices)
410
- # 2. remote address as a string
411
- # 3. remote port as an unsigned short
412
- # 4. state as a varint (should be 1 for status)
413
- # 2. Server responds with:
414
- # 2a. \x00 (JSON response)
415
- # An example JSON string contains:
416
- # {'players': {'max': 20, 'online': 0},
417
- # 'version': {'protocol': 404, 'name': '1.13.2'},
418
- # '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
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
- # 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
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 servers communicate as follows for an unconnected ping request:
511
- # 1. Client sends:
512
- # 1a. \x01 (unconnected ping packet containing the fields specified below)
513
- # 1b. current time as a long
514
- # 1c. magic number
515
- # 1d. client GUID as a long
516
- # 2. Server responds with:
517
- # 2a. \x1c (unconnected pong packet containing the follow fields)
518
- # 2b. current time as a long
519
- # 2c. server GUID as a long
520
- # 2d. 16-bit magic number
521
- # 2e. server ID string length
522
- # 2f. server ID as a string
523
- # The fields from the pong response, in order, are:
524
- # * edition
525
- # * MotD line 1
526
- # * protocol version
527
- # * version name
528
- # * current player count
529
- # * maximum player count
530
- # * unique server ID
531
- # * MotD line 2
532
- # * game mode as a string
533
- # * game mode as a numeric
534
- # * IPv4 port number
535
- # * 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
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
- # Returns the Minecraft server IP
792
+ # Address (hostname or IP address) of the Minecraft server
565
793
  attr_reader :address
566
794
 
567
- # Returns the Minecraft server TCP port
795
+ # Port (TCP or UDP) of the Minecraft server
568
796
  attr_reader :port
569
797
 
570
- # Returns a boolean describing whether the server is online or offline
798
+ # Whether or not the Minecraft server is online
571
799
  attr_reader :online
572
800
 
573
- # Returns the Minecraft version that the server is running
801
+ # Minecraft server version
574
802
  attr_reader :version
575
803
 
576
- # Returns the game mode (Bedrock/Pocket Edition only)
804
+ # Game mode
805
+ # @note Bedrock/Pocket Edition only
806
+ # @since 2.2.0
577
807
  attr_reader :mode
578
808
 
579
- # Returns the full version of the MotD
580
- #
581
- # 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
582
812
  attr_reader :motd
583
813
 
584
- # 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
585
817
  attr_reader :stripped_motd
586
818
 
587
- # Returns the current player count
819
+ # Current player count
588
820
  attr_reader :current_players
589
821
 
590
- # Returns the maximum player count
822
+ # Maximum player limit
591
823
  attr_reader :max_players
592
824
 
593
- # Returns the protocol level
594
- #
595
- # This is arbitrary and varies by Minecraft version.
596
- # However, multiple Minecraft versions can share the same
597
- # 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)
598
837
  attr_reader :protocol
599
838
 
600
- # Returns the complete JSON response data for queries to Minecraft
601
- # 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
602
842
  attr_reader :json_data
603
843
 
604
- # 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
605
847
  attr_reader :favicon_b64
606
848
 
607
- # 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
608
852
  attr_reader :favicon
609
853
 
610
- # Returns the ping time to the server in ms
854
+ # Ping time to the server in milliseconds (ms)
855
+ # @since 0.2.1
611
856
  attr_reader :latency
612
857
 
613
- # 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
614
863
  attr_reader :request_type
615
864
 
616
- # Returns the connection status
865
+ # Connection status
866
+ # @since 2.2.2
617
867
  attr_reader :connection_status
618
868
 
619
- # Returns whether or not all ping protocols should be attempted
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