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.
- checksums.yaml +5 -5
- data/lib/minestat.rb +350 -32
- metadata +14 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2bb7df47ea2b1dbc9593527397307a00e553ca5f67e7569d9c3d92bf26c971ef
|
4
|
+
data.tar.gz: 6dd403da69ee5878a58e7b1c973b937f57f04f607d86774fb87febd46514db54
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
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:
|
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:
|
12
|
+
date: 2021-09-08 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
|
-
description:
|
15
|
-
|
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/
|
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.
|
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: []
|