minestat 0.1.1 → 2.1.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 (3) hide show
  1. checksums.yaml +5 -5
  2. data/lib/minestat.rb +350 -32
  3. metadata +14 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: bbf26994bd72901b9d88df8e46c79b40e5640072
4
- data.tar.gz: f62193b7fb9b34246e82b99dcd2afb0d9441596e
2
+ SHA256:
3
+ metadata.gz: 2bb7df47ea2b1dbc9593527397307a00e553ca5f67e7569d9c3d92bf26c971ef
4
+ data.tar.gz: 6dd403da69ee5878a58e7b1c973b937f57f04f607d86774fb87febd46514db54
5
5
  SHA512:
6
- metadata.gz: 17c83ee983b651a642d42033ee9f7ee5e9841132d8cdd097be845fef9eef70bb009bca2c070fcecd9015cb1cddcad61585799138f0f55eaaef573f7430ce11eb
7
- data.tar.gz: fe11210ae33495c71478e741c4ee86c5f2ca141c1f220e3eb3e1a1befb8f1eaa0189892187414e12cfb180a77aec038f36d065abdc45fab6b2c529b091453d7a
6
+ metadata.gz: 54c7a4cfd1fb9c07e83529f513dcb800dbd2464044389a18c08f22c8bb7435f963fdfc5acacdd174774f9b65a3dec22391f76f1e856499ca48a64f4f5e03cd9e
7
+ data.tar.gz: ce1ae19de3e86ded84a5205ec38c50218181add5eaf5965e8578102299c7cd51e316a37bd58437abb2aa6f154d6b067644a0a6485fb9c823972572d3ded02a69
data/lib/minestat.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # minestat.rb - A Minecraft server status checker
2
- # Copyright (C) 2014, 2016 Lloyd Dilley
2
+ # Copyright (C) 2014-2021 Lloyd Dilley
3
3
  # http://www.dilley.me/
4
4
  #
5
5
  # This program is free software; you can redistribute it and/or modify
@@ -16,52 +16,370 @@
16
16
  # with this program; if not, write to the Free Software Foundation, Inc.,
17
17
  # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
 
19
+ require 'json'
19
20
  require 'socket'
20
21
  require 'timeout'
21
22
 
22
23
  class MineStat
23
- NUM_FIELDS = 6 # number of values expected from server
24
-
25
- def initialize(address, port, timeout = 7)
26
- @address = address
27
- @port = port
28
- @online # online or offline?
29
- @version # server version
30
- @motd # message of the day
31
- @current_players # current number of players online
32
- @max_players # maximum player capacity
33
-
34
- # Connect to the server and get the data
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
30
+
31
+ module Retval
32
+ SUCCESS = 0
33
+ CONNFAIL = -1
34
+ TIMEOUT = -2
35
+ UNKNOWN = -3
36
+ end
37
+
38
+ module Request
39
+ NONE = -1
40
+ BETA = 0
41
+ LEGACY = 1
42
+ EXTENDED = 2
43
+ JSON = 3
44
+ end
45
+
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
61
+
62
+ case request_type
63
+ when Request::BETA
64
+ retval = beta_request()
65
+ when Request::LEGACY
66
+ retval = legacy_request()
67
+ when Request::EXTENDED
68
+ retval = extended_legacy_request()
69
+ when Request::JSON
70
+ retval = json_request()
71
+ 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
75
+ # however since it may be due to an issue during the handshake.
76
+ # 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
+ # SLP 1.4/1.5
79
+ retval = legacy_request()
80
+ # SLP 1.8b/1.3
81
+ unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
82
+ retval = beta_request()
83
+ end
84
+ # SLP 1.6
85
+ unless retval == Retval::CONNFAIL
86
+ retval = extended_legacy_request()
87
+ end
88
+ # SLP 1.7
89
+ unless retval == Retval::CONNFAIL
90
+ retval = json_request()
91
+ end
92
+ end
93
+ @online = false unless retval == Retval::SUCCESS
94
+ end
95
+
96
+ # 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
101
+ @stripped_motd = @motd['text']
102
+ json_data = @motd['extra']
103
+ unless json_data.nil? || json_data.empty?
104
+ json_data.each do |nested_hash|
105
+ @stripped_motd += nested_hash['text']
106
+ end
107
+ end
108
+ @stripped_motd = @stripped_motd.gsub(/§./, "")
109
+ end
110
+ end
111
+
112
+ # Connects to remote server
113
+ def connect()
35
114
  begin
36
- data = nil
37
- Timeout::timeout(timeout) do
38
- server = TCPSocket.new(address, port)
39
- server.write("\xFE\x01")
40
- data=server.gets()
41
- server.close()
115
+ start_time = Time.now
116
+ @server = TCPSocket.new(@address, @port)
117
+ @latency = ((Time.now - start_time) * 1000).round
118
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
119
+ return Retval::CONNFAIL
120
+ rescue => exception
121
+ $stderr.puts exception
122
+ return Retval::UNKNOWN
123
+ end
124
+ return Retval::SUCCESS
125
+ end
126
+
127
+ # Populates object fields after connecting
128
+ def parse_data(delimiter, is_beta = false)
129
+ data = nil
130
+ 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
42
138
  end
43
- rescue Errno::ECONNREFUSED => e
44
- @online = false
45
- rescue => e # timeout is handled here
46
- @online = false
139
+ rescue => exception
140
+ $stderr.puts exception
141
+ return Retval::UNKNOWN
47
142
  end
48
143
 
49
- # Parse the received data
50
144
  if data == nil || data.empty?
51
- @online = false
145
+ return Retval::UNKNOWN
146
+ end
147
+
148
+ server_info = data.split(delimiter)
149
+ if is_beta
150
+ if server_info != nil && server_info.length >= NUM_FIELDS_BETA
151
+ @version = ">=1.8b/1.3" # since server does not return version, set it
152
+ @motd = server_info[0]
153
+ strip_motd
154
+ @current_players = server_info[1].to_i
155
+ @max_players = server_info[2].to_i
156
+ @online = true
157
+ else
158
+ return Retval::UNKNOWN
159
+ end
52
160
  else
53
- server_info = data.split("\x00\x00\x00")
54
161
  if server_info != nil && server_info.length >= NUM_FIELDS
162
+ # server_info[0] contains the section symbol and 1
163
+ @protocol = server_info[1].to_i # contains the protocol version (51 for 1.9 or 78 for 1.6.4 for example)
164
+ @version = server_info[2]
165
+ @motd = server_info[3]
166
+ strip_motd
167
+ @current_players = server_info[4].to_i
168
+ @max_players = server_info[5].to_i
55
169
  @online = true
56
- @version = server_info[2].gsub("\x00",'')
57
- @motd = server_info[3].gsub("\x00",'')
58
- @current_players = server_info[4].gsub("\x00",'')
59
- @max_players = server_info[5].gsub("\x00",'')
60
170
  else
61
- @online = false
171
+ return Retval::UNKNOWN
62
172
  end
63
173
  end
174
+ return Retval::SUCCESS
175
+ end
176
+
177
+ # 1.8b/1.3
178
+ # 1.8 beta through 1.3 servers communicate as follows for a ping request:
179
+ # 1. Client sends \xFE (server list ping)
180
+ # 2. Server responds with:
181
+ # 2a. \xFF (kick packet)
182
+ # 2b. data length
183
+ # 2c. 3 fields delimited by \u00A7 (section symbol)
184
+ # The 3 fields, in order, are: message of the day, current players, and max players
185
+ def beta_request()
186
+ retval = nil
187
+ begin
188
+ Timeout::timeout(@timeout) do
189
+ retval = connect()
190
+ return retval unless retval == Retval::SUCCESS
191
+ # Perform handshake and acquire data
192
+ @request_type = "SLP 1.8b/1.3 (beta)"
193
+ @server.write("\xFE")
194
+ retval = parse_data("\u00A7", true) # section symbol
195
+ end
196
+ rescue Timeout::Error
197
+ return Retval::TIMEOUT
198
+ rescue => exception
199
+ $stderr.puts exception
200
+ return Retval::UNKNOWN
201
+ end
202
+ return retval
203
+ end
204
+
205
+ # 1.4/1.5
206
+ # 1.4 and 1.5 servers communicate as follows for a ping request:
207
+ # 1. Client sends:
208
+ # 1a. \xFE (server list ping)
209
+ # 1b. \x01 (server list ping payload)
210
+ # 2. Server responds with:
211
+ # 2a. \xFF (kick packet)
212
+ # 2b. data length
213
+ # 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
216
+ # The protocol version corresponds with the server version and can be the
217
+ # same for different server versions.
218
+ def legacy_request()
219
+ retval = nil
220
+ begin
221
+ Timeout::timeout(@timeout) do
222
+ retval = connect()
223
+ return retval unless retval == Retval::SUCCESS
224
+ # Perform handshake and acquire data
225
+ @request_type = "SLP 1.4/1.5 (legacy)"
226
+ @server.write("\xFE\x01")
227
+ retval = parse_data("\x00") # null
228
+ end
229
+ rescue Timeout::Error
230
+ return Retval::TIMEOUT
231
+ rescue => exception
232
+ $stderr.puts exception
233
+ return Retval::UNKNOWN
234
+ end
235
+ return retval
236
+ end
237
+
238
+ # 1.6
239
+ # 1.6 servers communicate as follows for a ping request:
240
+ # 1. Client sends:
241
+ # 1a. \xFE (server list ping)
242
+ # 1b. \x01 (server list ping payload)
243
+ # 1c. \xFA (plugin message)
244
+ # 1d. \x00\x0B (11 which is the length of "MC|PingHost")
245
+ # 1e. "MC|PingHost" encoded as a UTF-16BE string
246
+ # 1f. length of remaining data as a short: remote address (encoded as UTF-16BE) + 7
247
+ # 1g. arbitrary 1.6 protocol version (\x4E for example for 78)
248
+ # 1h. length of remote address as a short
249
+ # 1i. remote address encoded as a UTF-16BE string
250
+ # 1j. remote port as an int
251
+ # 2. Server responds with:
252
+ # 2a. \xFF (kick packet)
253
+ # 2b. data length
254
+ # 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
257
+ # The protocol version corresponds with the server version and can be the
258
+ # same for different server versions.
259
+ def extended_legacy_request()
260
+ retval = nil
261
+ begin
262
+ Timeout::timeout(@timeout) do
263
+ retval = connect()
264
+ return retval unless retval == Retval::SUCCESS
265
+ # Perform handshake and acquire data
266
+ @request_type = "SLP 1.6 (extended legacy)"
267
+ @server.write("\xFE\x01\xFA")
268
+ @server.write("\x00\x0B") # 11 (length of "MC|PingHost")
269
+ @server.write('MC|PingHost'.encode('UTF-16BE').force_encoding('ASCII-8BIT'))
270
+ @server.write([7 + 2 * @address.length].pack('n'))
271
+ @server.write("\x4E") # 78 (protocol version of 1.6.4)
272
+ @server.write([@address.length].pack('n'))
273
+ @server.write(@address.encode('UTF-16BE').force_encoding('ASCII-8BIT'))
274
+ @server.write([@port].pack('N'))
275
+ retval = parse_data("\x00") # null
276
+ end
277
+ rescue Timeout::Error
278
+ return Retval::TIMEOUT
279
+ rescue => exception
280
+ $stderr.puts exception
281
+ return Retval::UNKNOWN
282
+ end
283
+ return retval
284
+ end
285
+
286
+ # 1.7
287
+ # 1.7 to current servers communicate as follows for a ping request:
288
+ # 1. Client sends:
289
+ # 1a. \x00 (handshake packet containing the fields specified below)
290
+ # 1b. \x00 (request)
291
+ # The handshake packet contains the following fields respectively:
292
+ # 1. protocol version as a varint (\x00 suffices)
293
+ # 2. remote address as a string
294
+ # 3. remote port as an unsigned short
295
+ # 4. state as a varint (should be 1 for status)
296
+ # 2. Server responds with:
297
+ # 2a. \x00 (JSON response)
298
+ # An example JSON string contains:
299
+ # {'players': {'max': 20, 'online': 0},
300
+ # 'version': {'protocol': 404, 'name': '1.13.2'},
301
+ # 'description': {'text': 'A Minecraft Server'}}
302
+ def json_request()
303
+ retval = nil
304
+ begin
305
+ Timeout::timeout(@timeout) do
306
+ retval = connect()
307
+ return retval unless retval == Retval::SUCCESS
308
+ # Perform handshake
309
+ @request_type = "SLP 1.7 (JSON)"
310
+ payload = "\x00\x00"
311
+ payload += [@address.length].pack('c') << @address
312
+ payload += [@port].pack('n')
313
+ payload += "\x01"
314
+ payload = [payload.length].pack('c') << payload
315
+ @server.write(payload)
316
+ @server.write("\x01\x00")
317
+ @server.flush
318
+
319
+ # Acquire data
320
+ _total_len = unpack_varint
321
+ return Retval::UNKNOWN if unpack_varint != 0
322
+ json_len = unpack_varint
323
+ json_data = recv_json(json_len)
324
+ @server.close
325
+
326
+ # Parse data
327
+ json_data = JSON.parse(json_data)
328
+ @json_data = json_data
329
+ @protocol = json_data['version']['protocol'].to_i
330
+ @version = json_data['version']['name']
331
+ @motd = json_data['description']
332
+ strip_motd(true)
333
+ @current_players = json_data['players']['online'].to_i
334
+ @max_players = json_data['players']['max'].to_i
335
+ if !@version.empty? && !@motd.empty? && !@current_players.nil? && !@max_players.nil?
336
+ @online = true
337
+ else
338
+ retval = Retval::UNKNOWN
339
+ end
340
+ end
341
+ rescue Timeout::Error
342
+ return Retval::TIMEOUT
343
+ rescue JSON::ParserError
344
+ return Retval::UNKNOWN
345
+ rescue => exception
346
+ $stderr.puts exception
347
+ return Retval::UNKNOWN
348
+ end
349
+ return retval
350
+ end
351
+
352
+ # Reads JSON data from the socket
353
+ def recv_json(json_len)
354
+ json_data = ""
355
+ begin
356
+ loop do
357
+ remaining = json_len - json_data.length
358
+ data = @server.recv(remaining)
359
+ @server.flush
360
+ json_data += data
361
+ break if json_data.length >= json_len
362
+ end
363
+ rescue => exception
364
+ $stderr.puts exception
365
+ end
366
+ return json_data
367
+ end
368
+
369
+ # Returns value of varint type
370
+ def unpack_varint()
371
+ vint = 0
372
+ i = 0
373
+ while i <= MAX_VARINT_SIZE
374
+ data = @server.read(1)
375
+ return 0 if data.nil? || data.empty?
376
+ data = data.ord
377
+ vint |= (data & 0x7F) << 7 * i
378
+ break if (data & 0x80) != 128
379
+ i += 1
380
+ end
381
+ return vint
64
382
  end
65
383
 
66
- attr_reader :address, :port, :online, :version, :motd, :current_players, :max_players
384
+ attr_reader :address, :port, :online, :version, :motd, :stripped_motd, :current_players, :max_players, :protocol, :json_data, :latency, :request_type
67
385
  end
metadata CHANGED
@@ -1,27 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minestat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lloyd Dilley
8
8
  - Stepan Melnikov
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-12-28 00:00:00.000000000 Z
12
+ date: 2021-09-08 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description:
15
- email:
14
+ description: MineStat polls Minecraft server data such as version, motd, current players,
15
+ and max players.
16
+ email:
17
+ - minecraft@frag.land
16
18
  executables: []
17
19
  extensions: []
18
20
  extra_rdoc_files: []
19
21
  files:
20
22
  - lib/minestat.rb
21
- homepage: https://github.com/ldilley/minestat
22
- licenses: []
23
+ homepage: https://github.com/FragLand/minestat
24
+ licenses:
25
+ - GPL-3.0
23
26
  metadata: {}
24
- post_install_message:
27
+ post_install_message:
25
28
  rdoc_options: []
26
29
  require_paths:
27
30
  - lib
@@ -36,9 +39,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
36
39
  - !ruby/object:Gem::Version
37
40
  version: '0'
38
41
  requirements: []
39
- rubyforge_project:
40
- rubygems_version: 2.6.8
41
- signing_key:
42
+ rubyforge_project:
43
+ rubygems_version: 2.7.6.2
44
+ signing_key:
42
45
  specification_version: 4
43
46
  summary: Minecraft server status checker
44
47
  test_files: []