minestat 2.1.0 → 2.2.1

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 (3) hide show
  1. checksums.yaml +5 -5
  2. data/lib/minestat.rb +250 -55
  3. metadata +7 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 2bb7df47ea2b1dbc9593527397307a00e553ca5f67e7569d9c3d92bf26c971ef
4
- data.tar.gz: 6dd403da69ee5878a58e7b1c973b937f57f04f607d86774fb87febd46514db54
2
+ SHA1:
3
+ metadata.gz: e0acf4a379eb97f4145d734b8f7d3b82b9b5f57d
4
+ data.tar.gz: 8fe0222fc8892a44eea98917e87eaad75f0fd8d3
5
5
  SHA512:
6
- metadata.gz: 54c7a4cfd1fb9c07e83529f513dcb800dbd2464044389a18c08f22c8bb7435f963fdfc5acacdd174774f9b65a3dec22391f76f1e856499ca48a64f4f5e03cd9e
7
- data.tar.gz: ce1ae19de3e86ded84a5205ec38c50218181add5eaf5965e8578102299c7cd51e316a37bd58437abb2aa6f154d6b067644a0a6485fb9c823972572d3ded02a69
6
+ metadata.gz: 071fd966c47fd93d01087a4c85d20ca2ca56c05cec1d22c8aeff824602c900f085d7c877e37b0dfe25d3105f74dab2a52d2fa1f19b8706d3a31d16bbb96a7af8
7
+ data.tar.gz: 4846ca755878fd8d81bddd5634916e83955b142ea587cb81c3ba55a629f04129a2eacdc11f7fa68d9b6d77f5ab8ed141628d330a7e33a5e2fc82e46d61b625ab
data/lib/minestat.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # minestat.rb - A Minecraft server status checker
2
- # Copyright (C) 2014-2021 Lloyd Dilley
2
+ # Copyright (C) 2014-2022 Lloyd Dilley
3
3
  # http://www.dilley.me/
4
4
  #
5
5
  # This program is free software; you can redistribute it and/or modify
@@ -20,44 +20,84 @@ require 'json'
20
20
  require 'socket'
21
21
  require 'timeout'
22
22
 
23
+ ##
24
+ # Provides a ruby interface for polling Minecraft server status.
23
25
  class MineStat
24
- VERSION = "2.1.0" # MineStat version
25
- NUM_FIELDS = 6 # number of values expected from server
26
- NUM_FIELDS_BETA = 3 # number of values expected from a 1.8b/1.3 server
27
- MAX_VARINT_SIZE = 5 # maximum number of bytes a varint can be
28
- DEFAULT_PORT = 25565 # default TCP port
29
- DEFAULT_TIMEOUT = 5 # default TCP timeout in seconds
26
+ # MineStat version
27
+ VERSION = "2.2.1"
28
+ # Number of values expected from server
29
+ NUM_FIELDS = 6
30
+ # Number of values expected from a 1.8b/1.3 server
31
+ NUM_FIELDS_BETA = 3
32
+ # Maximum number of bytes a varint can be
33
+ MAX_VARINT_SIZE = 5
34
+ # Default TCP port
35
+ DEFAULT_TCP_PORT = 25565
36
+ # Bedrock/Pocket Edition default UDP port
37
+ DEFAULT_BEDROCK_PORT = 19132
38
+ # Default TCP/UDP timeout in seconds
39
+ DEFAULT_TIMEOUT = 5
40
+ # Bedrock/Pocket Edition packet offset in bytes (1 + 8 + 8 + 16 + 2)
41
+ # Unconnected pong (0x1C) = 1 byte
42
+ # Timestamp as a long = 8 bytes
43
+ # Server GUID as a long = 8 bytes
44
+ # Magic number = 16 bytes
45
+ # String ID length = 2 bytes
46
+ BEDROCK_PACKET_OFFSET = 35
30
47
 
48
+ ##
49
+ # Stores constants that represent the results of a server ping
31
50
  module Retval
51
+ # The server ping completed successfully
32
52
  SUCCESS = 0
53
+ # The server ping failed due to a connection error
33
54
  CONNFAIL = -1
55
+ # The server ping failed due to a connection time out
34
56
  TIMEOUT = -2
57
+ # The server ping failed for an unknown reason
35
58
  UNKNOWN = -3
36
59
  end
37
60
 
61
+ ##
62
+ # Stores constants that represent the different kinds of server
63
+ # list pings/requests that a Minecraft server might expect when
64
+ # being polled for status information.
38
65
  module Request
66
+ # Try everything
39
67
  NONE = -1
68
+ # Server versions 1.8b to 1.3
40
69
  BETA = 0
70
+ # Server versions 1.4 to 1.5
41
71
  LEGACY = 1
72
+ # Server version 1.6
42
73
  EXTENDED = 2
74
+ # Server versions 1.7 to latest
43
75
  JSON = 3
76
+ # Bedrock/Pocket Edition
77
+ BEDROCK = 4
44
78
  end
45
79
 
46
- def initialize(address, port = DEFAULT_PORT, timeout = DEFAULT_TIMEOUT, request_type = Request::NONE)
47
- @address = address # address of server
48
- @port = port # TCP port of server
49
- @online # online or offline?
50
- @version # server version
51
- @motd # message of the day
52
- @stripped_motd # message of the day without formatting
53
- @current_players # current number of players online
54
- @max_players # maximum player capacity
55
- @protocol # protocol level
56
- @json_data # JSON data for 1.7 queries
57
- @latency # ping time to server in milliseconds
58
- @timeout = timeout # TCP timeout
59
- @server # server socket
60
- @request_type # SLP protocol version
80
+ ##
81
+ # Instantiate an instance of MineStat and poll the specified server for information
82
+ def initialize(address, port = DEFAULT_TCP_PORT, timeout = DEFAULT_TIMEOUT, request_type = Request::NONE)
83
+ @address = address # address of server
84
+ @port = port # TCP/UDP port of server
85
+ @online # online or offline?
86
+ @version # server version
87
+ @mode = "Unspecified" # game mode (Bedrock/Pocket Edition only)
88
+ @motd # message of the day
89
+ @stripped_motd # message of the day without formatting
90
+ @current_players # current number of players online
91
+ @max_players # maximum player capacity
92
+ @protocol # protocol level
93
+ @json_data # JSON data for 1.7 queries
94
+ @latency # ping time to server in milliseconds
95
+ @timeout = timeout # TCP/UDP timeout
96
+ @server # server socket
97
+ @request_type # Protocol version
98
+ @try_all = false # try all protocols?
99
+
100
+ @try_all = true if request_type == Request::NONE
61
101
 
62
102
  case request_type
63
103
  when Request::BETA
@@ -68,13 +108,14 @@ class MineStat
68
108
  retval = extended_legacy_request()
69
109
  when Request::JSON
70
110
  retval = json_request()
111
+ when Request::BEDROCK
112
+ retval = bedrock_request()
71
113
  else
72
- # Attempt various SLP ping requests in a particular order. If the request
73
- # succeeds or the connection fails, there is no reason to continue with
74
- # subsequent requests. Attempts should continue in the event of a timeout
114
+ # Attempt various ping requests in a particular order. If the
115
+ # connection fails, there is no reason to continue with subsequent
116
+ # requests. Attempts should continue in the event of a timeout
75
117
  # however since it may be due to an issue during the handshake.
76
118
  # Note: Newer server versions may still respond to older SLP requests.
77
- # For example, 1.13.2 responds to 1.4/1.5 queries, but not 1.6 queries.
78
119
  # SLP 1.4/1.5
79
120
  retval = legacy_request()
80
121
  # SLP 1.8b/1.3
@@ -89,31 +130,46 @@ class MineStat
89
130
  unless retval == Retval::CONNFAIL
90
131
  retval = json_request()
91
132
  end
133
+ # Bedrock/Pocket Edition
134
+ unless retval == Retval::CONNFAIL
135
+ retval = bedrock_request()
136
+ end
92
137
  end
93
138
  @online = false unless retval == Retval::SUCCESS
94
139
  end
95
140
 
96
141
  # Strips message of the day formatting characters
97
- def strip_motd(is_json = false)
98
- unless is_json
99
- @stripped_motd = @motd.gsub(/§./, "")
100
- else
142
+ def strip_motd()
143
+ unless @motd['text'] == nil
101
144
  @stripped_motd = @motd['text']
145
+ else
146
+ @stripped_motd = @motd
147
+ end
148
+ unless @motd['extra'] == nil
102
149
  json_data = @motd['extra']
103
150
  unless json_data.nil? || json_data.empty?
104
151
  json_data.each do |nested_hash|
105
152
  @stripped_motd += nested_hash['text']
106
153
  end
107
154
  end
108
- @stripped_motd = @stripped_motd.gsub(/§./, "")
109
155
  end
156
+ @stripped_motd = @stripped_motd.force_encoding('UTF-8')
157
+ @stripped_motd = @stripped_motd.gsub(/§./, "")
110
158
  end
111
159
 
112
- # Connects to remote server
160
+ ##
161
+ # Establishes a connection to the Minecraft server
113
162
  def connect()
114
163
  begin
115
- start_time = Time.now
116
- @server = TCPSocket.new(@address, @port)
164
+ if @request_type == Request::BEDROCK || @request_type == "Bedrock/Pocket Edition"
165
+ @port = DEFAULT_BEDROCK_PORT if @port == DEFAULT_TCP_PORT && @try_all
166
+ start_time = Time.now
167
+ @server = UDPSocket.new
168
+ @server.connect(@address, @port)
169
+ else
170
+ start_time = Time.now
171
+ @server = TCPSocket.new(@address, @port)
172
+ end
117
173
  @latency = ((Time.now - start_time) * 1000).round
118
174
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
119
175
  return Retval::CONNFAIL
@@ -128,16 +184,28 @@ class MineStat
128
184
  def parse_data(delimiter, is_beta = false)
129
185
  data = nil
130
186
  begin
131
- if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
132
- len = @server.read(2).unpack('n').first
133
- data = @server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
134
- @server.close
135
- else
136
- @server.close
137
- return Retval::UNKNOWN
187
+ if @request_type == "Bedrock/Pocket Edition"
188
+ if @server.recv(1, Socket::MSG_PEEK).unpack('C').first == 0x1C # unconnected pong packet
189
+ server_id_len = @server.recv(BEDROCK_PACKET_OFFSET, Socket::MSG_PEEK)[-2..-1].unpack('n').first
190
+ data = @server.recv(BEDROCK_PACKET_OFFSET + server_id_len)[BEDROCK_PACKET_OFFSET..-1]
191
+ @server.close
192
+ else
193
+ @server.close
194
+ return Retval::UNKNOWN
195
+ end
196
+ else # SLP
197
+ if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
198
+ len = @server.read(2).unpack('n').first
199
+ data = @server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
200
+ @server.close
201
+ else
202
+ @server.close
203
+ return Retval::UNKNOWN
204
+ end
138
205
  end
139
- rescue => exception
140
- $stderr.puts exception
206
+ rescue
207
+ #rescue => exception
208
+ #$stderr.puts exception
141
209
  return Retval::UNKNOWN
142
210
  end
143
211
 
@@ -157,7 +225,20 @@ class MineStat
157
225
  else
158
226
  return Retval::UNKNOWN
159
227
  end
160
- else
228
+ elsif @request_type == "Bedrock/Pocket Edition"
229
+ if server_info != nil
230
+ @protocol = server_info[2].to_i
231
+ @version = "#{server_info[3]} #{server_info[7]} (#{server_info[0]})"
232
+ @mode = server_info[8]
233
+ @motd = server_info[1]
234
+ strip_motd
235
+ @current_players = server_info[4].to_i
236
+ @max_players = server_info[5].to_i
237
+ @online = true
238
+ else
239
+ return Retval::UNKNOWN
240
+ end
241
+ else # SLP
161
242
  if server_info != nil && server_info.length >= NUM_FIELDS
162
243
  # server_info[0] contains the section symbol and 1
163
244
  @protocol = server_info[1].to_i # contains the protocol version (51 for 1.9 or 78 for 1.6.4 for example)
@@ -174,14 +255,17 @@ class MineStat
174
255
  return Retval::SUCCESS
175
256
  end
176
257
 
177
- # 1.8b/1.3
258
+ ##
178
259
  # 1.8 beta through 1.3 servers communicate as follows for a ping request:
179
260
  # 1. Client sends \xFE (server list ping)
180
261
  # 2. Server responds with:
181
262
  # 2a. \xFF (kick packet)
182
263
  # 2b. data length
183
264
  # 2c. 3 fields delimited by \u00A7 (section symbol)
184
- # The 3 fields, in order, are: message of the day, current players, and max players
265
+ # The 3 fields, in order, are:
266
+ # * message of the day
267
+ # * current players
268
+ # * max players
185
269
  def beta_request()
186
270
  retval = nil
187
271
  begin
@@ -202,7 +286,7 @@ class MineStat
202
286
  return retval
203
287
  end
204
288
 
205
- # 1.4/1.5
289
+ ##
206
290
  # 1.4 and 1.5 servers communicate as follows for a ping request:
207
291
  # 1. Client sends:
208
292
  # 1a. \xFE (server list ping)
@@ -211,8 +295,14 @@ class MineStat
211
295
  # 2a. \xFF (kick packet)
212
296
  # 2b. data length
213
297
  # 2c. 6 fields delimited by \x00 (null)
214
- # The 6 fields, in order, are: the section symbol and 1, protocol version,
215
- # server version, message of the day, current players, and max players
298
+ # The 6 fields, in order, are:
299
+ # * the section symbol and 1
300
+ # * protocol version
301
+ # * server version
302
+ # * message of the day
303
+ # * current players
304
+ # * max players
305
+ #
216
306
  # The protocol version corresponds with the server version and can be the
217
307
  # same for different server versions.
218
308
  def legacy_request()
@@ -235,7 +325,7 @@ class MineStat
235
325
  return retval
236
326
  end
237
327
 
238
- # 1.6
328
+ ##
239
329
  # 1.6 servers communicate as follows for a ping request:
240
330
  # 1. Client sends:
241
331
  # 1a. \xFE (server list ping)
@@ -252,8 +342,14 @@ class MineStat
252
342
  # 2a. \xFF (kick packet)
253
343
  # 2b. data length
254
344
  # 2c. 6 fields delimited by \x00 (null)
255
- # The 6 fields, in order, are: the section symbol and 1, protocol version,
256
- # server version, message of the day, current players, and max players
345
+ # The 6 fields, in order, are:
346
+ # * the section symbol and 1
347
+ # * protocol version
348
+ # * server version
349
+ # * message of the day
350
+ # * current players
351
+ # * max players
352
+ #
257
353
  # The protocol version corresponds with the server version and can be the
258
354
  # same for different server versions.
259
355
  def extended_legacy_request()
@@ -283,7 +379,7 @@ class MineStat
283
379
  return retval
284
380
  end
285
381
 
286
- # 1.7
382
+ ##
287
383
  # 1.7 to current servers communicate as follows for a ping request:
288
384
  # 1. Client sends:
289
385
  # 1a. \x00 (handshake packet containing the fields specified below)
@@ -329,7 +425,7 @@ class MineStat
329
425
  @protocol = json_data['version']['protocol'].to_i
330
426
  @version = json_data['version']['name']
331
427
  @motd = json_data['description']
332
- strip_motd(true)
428
+ strip_motd
333
429
  @current_players = json_data['players']['online'].to_i
334
430
  @max_players = json_data['players']['max'].to_i
335
431
  if !@version.empty? && !@motd.empty? && !@current_players.nil? && !@max_players.nil?
@@ -381,5 +477,104 @@ class MineStat
381
477
  return vint
382
478
  end
383
479
 
384
- attr_reader :address, :port, :online, :version, :motd, :stripped_motd, :current_players, :max_players, :protocol, :json_data, :latency, :request_type
480
+ ##
481
+ # Bedrock/Pocket Edition servers communicate as follows for an unconnected ping request:
482
+ # 1. Client sends:
483
+ # 1a. \x01 (unconnected ping packet containing the fields specified below)
484
+ # 1b. current time as a long
485
+ # 1c. magic number
486
+ # 1d. client GUID as a long
487
+ # 2. Server responds with:
488
+ # 2a. \x1c (unconnected pong packet containing the follow fields)
489
+ # 2b. current time as a long
490
+ # 2c. server GUID as a long
491
+ # 2d. 16-bit magic number
492
+ # 2e. server ID string length
493
+ # 2f. server ID as a string
494
+ # The fields from the pong response, in order, are:
495
+ # * edition
496
+ # * MotD line 1
497
+ # * protocol version
498
+ # * version name
499
+ # * current player count
500
+ # * maximum player count
501
+ # * unique server ID
502
+ # * MotD line 2
503
+ # * game mode as a string
504
+ # * game mode as a numeric
505
+ # * IPv4 port number
506
+ # * IPv6 port number
507
+ def bedrock_request()
508
+ retval = nil
509
+ begin
510
+ Timeout::timeout(@timeout) do
511
+ @request_type = "Bedrock/Pocket Edition"
512
+ retval = connect()
513
+ return retval unless retval == Retval::SUCCESS
514
+ # Perform handshake and acquire data
515
+ payload = "\x01".force_encoding('ASCII-8BIT') # unconnected ping
516
+ payload += [Time.now.to_i].pack('L!<').force_encoding('ASCII-8BIT') # current time as a long
517
+ payload += "\x00\xFF\xFF\x00\xFE\xFE\xFE\xFE\xFD\xFD\xFD\xFD\x12\x34\x56\x78".force_encoding('ASCII-8BIT') # magic number
518
+ payload += [2].pack('L!<').force_encoding('ASCII-8BIT') # client GUID as a long
519
+ @server.write(payload)
520
+ @server.flush
521
+ retval = parse_data("\x3B") # semicolon
522
+ end
523
+ rescue Timeout::Error
524
+ return Retval::TIMEOUT
525
+ rescue => exception
526
+ $stderr.puts exception
527
+ return Retval::UNKNOWN
528
+ end
529
+ return retval
530
+ end
531
+
532
+ # Returns the Minecraft server IP
533
+ attr_reader :address
534
+
535
+ # Returns the Minecraft server TCP port
536
+ attr_reader :port
537
+
538
+ # Returns a boolean describing whether the server is online or offline
539
+ attr_reader :online
540
+
541
+ # Returns the Minecraft version that the server is running
542
+ attr_reader :version
543
+
544
+ # Returns the game mode (Bedrock/Pocket Edition only)
545
+ attr_reader :mode
546
+
547
+ # Returns the full version of the MotD
548
+ #
549
+ # If you just want the MotD text, use stripped_motd
550
+ attr_reader :motd
551
+
552
+ # Returns just the plain text contained within the MotD
553
+ attr_reader :stripped_motd
554
+
555
+ # Returns the current player count
556
+ attr_reader :current_players
557
+
558
+ # Returns the maximum player count
559
+ attr_reader :max_players
560
+
561
+ # Returns the protocol level
562
+ #
563
+ # This is arbitrary and varies by Minecraft version.
564
+ # However, multiple Minecraft versions can share the same
565
+ # protocol level
566
+ attr_reader :protocol
567
+
568
+ # Returns the complete JSON response data for queries to Minecraft
569
+ # servers with a version greater than or equal to 1.7
570
+ attr_reader :json_data
571
+
572
+ # Returns the ping time to the server in ms
573
+ attr_reader :latency
574
+
575
+ # Returns the protocol version
576
+ attr_reader :request_type
577
+
578
+ # Returns whether or not all ping protocols should be attempted
579
+ attr_reader :try_all
385
580
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minestat
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lloyd Dilley
8
- - Stepan Melnikov
9
- autorequire:
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2021-09-08 00:00:00.000000000 Z
11
+ date: 2022-05-15 00:00:00.000000000 Z
13
12
  dependencies: []
14
13
  description: MineStat polls Minecraft server data such as version, motd, current players,
15
14
  and max players.
@@ -24,7 +23,7 @@ homepage: https://github.com/FragLand/minestat
24
23
  licenses:
25
24
  - GPL-3.0
26
25
  metadata: {}
27
- post_install_message:
26
+ post_install_message:
28
27
  rdoc_options: []
29
28
  require_paths:
30
29
  - lib
@@ -39,9 +38,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
40
  requirements: []
42
- rubyforge_project:
43
- rubygems_version: 2.7.6.2
44
- signing_key:
41
+ rubyforge_project:
42
+ rubygems_version: 2.5.2.1
43
+ signing_key:
45
44
  specification_version: 4
46
45
  summary: Minecraft server status checker
47
46
  test_files: []