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.
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