minestat 0.2.1 → 0.3.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 +4 -4
- data/lib/minestat.rb +195 -118
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca86c11c59beb2c8fb7eefa8e4a6730b9463b6abdab1241201ba897b5a3e2eee
|
4
|
+
data.tar.gz: 51ae47ef99e1a524617d617ffb9c602f5a1673c4b8d6326f4e122e27d054c242
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f84a7ec58ae1382231820b26fcefc146bd71e31b35ef2e3d216836d70f04130c0e23b87e20e1c04f4e701b536335a53e5f6c7d0fad1c7bb89772a6f1d935609
|
7
|
+
data.tar.gz: 37a5c9628166579b945d3961c20cbae585964d981cb293bb03f4fe5222e148bd547a1a10044c58b007870210649a65f122aa5a96bcc0ca911fbe515d17d9cd07
|
data/lib/minestat.rb
CHANGED
@@ -16,12 +16,15 @@
|
|
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
|
NUM_FIELDS = 6 # number of values expected from server
|
24
25
|
NUM_FIELDS_BETA = 3 # number of values expected from a 1.8b/1.3 server
|
26
|
+
MAX_VARINT_SIZE = 5 # maximum number of bytes a varint can be
|
27
|
+
DEFAULT_PORT = 25565 # default TCP port
|
25
28
|
DEFAULT_TIMEOUT = 5 # default TCP timeout in seconds
|
26
29
|
|
27
30
|
module Retval
|
@@ -31,15 +34,20 @@ class MineStat
|
|
31
34
|
UNKNOWN = -3
|
32
35
|
end
|
33
36
|
|
34
|
-
def initialize(address, port, timeout = DEFAULT_TIMEOUT)
|
35
|
-
@address = address
|
36
|
-
@port = port
|
37
|
+
def initialize(address, port = DEFAULT_PORT, timeout = DEFAULT_TIMEOUT)
|
38
|
+
@address = address # address of server
|
39
|
+
@port = port # TCP port of server
|
37
40
|
@online # online or offline?
|
38
41
|
@version # server version
|
39
42
|
@motd # message of the day
|
40
43
|
@current_players # current number of players online
|
41
44
|
@max_players # maximum player capacity
|
45
|
+
@protocol # protocol level
|
46
|
+
@json_data # JSON data for 1.7 queries
|
42
47
|
@latency # ping time to server in milliseconds
|
48
|
+
@timeout = timeout # TCP timeout
|
49
|
+
@server # server socket
|
50
|
+
@query_type # SLP query version
|
43
51
|
|
44
52
|
# Try the newest protocol first and work down. If the query succeeds or the
|
45
53
|
# connection fails, there is no reason to continue with subsequent queries.
|
@@ -48,23 +56,86 @@ class MineStat
|
|
48
56
|
# Note: Newer server versions may still respond to older ping query types.
|
49
57
|
# For example, 1.13.2 responds to 1.4/1.5 queries, but not 1.6 queries.
|
50
58
|
# 1.7
|
51
|
-
retval = json_query(
|
59
|
+
retval = json_query()
|
52
60
|
# 1.6
|
53
61
|
unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
|
54
|
-
retval = new_query(
|
62
|
+
retval = new_query()
|
55
63
|
end
|
56
64
|
# 1.4/1.5
|
57
65
|
unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
|
58
|
-
retval = legacy_query(
|
66
|
+
retval = legacy_query()
|
59
67
|
end
|
60
68
|
# 1.8b/1.3
|
61
69
|
unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
|
62
|
-
retval = beta_query(
|
70
|
+
retval = beta_query()
|
63
71
|
end
|
64
72
|
|
65
73
|
@online = false unless retval == Retval::SUCCESS
|
66
74
|
end
|
67
75
|
|
76
|
+
# Connects to remote server
|
77
|
+
def connect()
|
78
|
+
begin
|
79
|
+
start_time = Time.now
|
80
|
+
@server = TCPSocket.new(@address, @port)
|
81
|
+
@latency = ((Time.now - start_time) * 1000).round
|
82
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
83
|
+
return Retval::CONNFAIL
|
84
|
+
rescue => exception
|
85
|
+
$stderr.puts exception
|
86
|
+
return Retval::UNKNOWN
|
87
|
+
end
|
88
|
+
return Retval::SUCCESS
|
89
|
+
end
|
90
|
+
|
91
|
+
# Populates object fields after connecting
|
92
|
+
def parse_data(delimiter, is_beta = false)
|
93
|
+
data = nil
|
94
|
+
begin
|
95
|
+
if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
|
96
|
+
len = @server.read(2).unpack('n').first
|
97
|
+
data = @server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
|
98
|
+
@server.close
|
99
|
+
else
|
100
|
+
@server.close
|
101
|
+
return Retval::UNKNOWN
|
102
|
+
end
|
103
|
+
rescue => exception
|
104
|
+
$stderr.puts exception
|
105
|
+
return Retval::UNKNOWN
|
106
|
+
end
|
107
|
+
|
108
|
+
if data == nil || data.empty?
|
109
|
+
return Retval::UNKNOWN
|
110
|
+
end
|
111
|
+
|
112
|
+
server_info = data.split(delimiter)
|
113
|
+
if is_beta
|
114
|
+
if server_info != nil && server_info.length >= NUM_FIELDS_BETA
|
115
|
+
@version = "1.8b/1.3" # since server does not return version, set it
|
116
|
+
@motd = server_info[0]
|
117
|
+
@current_players = server_info[1].to_i
|
118
|
+
@max_players = server_info[2].to_i
|
119
|
+
@online = true
|
120
|
+
else
|
121
|
+
return Retval::UNKNOWN
|
122
|
+
end
|
123
|
+
else
|
124
|
+
if server_info != nil && server_info.length >= NUM_FIELDS
|
125
|
+
# server_info[0] contains the section symbol and 1
|
126
|
+
@protocol = server_info[1].to_i # contains the protocol version (51 for 1.9 or 78 for 1.6.4 for example)
|
127
|
+
@version = server_info[2]
|
128
|
+
@motd = server_info[3]
|
129
|
+
@current_players = server_info[4].to_i
|
130
|
+
@max_players = server_info[5].to_i
|
131
|
+
@online = true
|
132
|
+
else
|
133
|
+
return Retval::UNKNOWN
|
134
|
+
end
|
135
|
+
end
|
136
|
+
return Retval::SUCCESS
|
137
|
+
end
|
138
|
+
|
68
139
|
# 1.8b/1.3
|
69
140
|
# 1.8 beta through 1.3 servers communicate as follows for a ping query:
|
70
141
|
# 1. Client sends \xFE (server list ping)
|
@@ -73,45 +144,24 @@ class MineStat
|
|
73
144
|
# 2b. data length
|
74
145
|
# 2c. 3 fields delimited by \u00A7 (section symbol)
|
75
146
|
# The 3 fields, in order, are: message of the day, current players, and max players
|
76
|
-
def beta_query(
|
147
|
+
def beta_query()
|
148
|
+
retval = nil
|
77
149
|
begin
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@
|
83
|
-
server.write("\xFE")
|
84
|
-
|
85
|
-
len = server.read(2).unpack('n').first
|
86
|
-
data = server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
|
87
|
-
server.close
|
88
|
-
else
|
89
|
-
server.close
|
90
|
-
return Retval::UNKNOWN
|
91
|
-
end
|
150
|
+
Timeout::timeout(@timeout) do
|
151
|
+
retval = connect()
|
152
|
+
return retval unless retval == Retval::SUCCESS
|
153
|
+
# Perform handshake and acquire data
|
154
|
+
@query_type = "1.8b/1.3"
|
155
|
+
@server.write("\xFE")
|
156
|
+
retval = parse_data("\u00A7", true) # section symbol
|
92
157
|
end
|
93
|
-
|
94
|
-
if data == nil || data.empty?
|
95
|
-
return Retval::UNKNOWN
|
96
|
-
else
|
97
|
-
server_info = data.split("\u00A7") # section symbol
|
98
|
-
if server_info != nil && server_info.length >= NUM_FIELDS_BETA
|
99
|
-
@version = "1.8b/1.3" # since server does not return version, set it
|
100
|
-
@motd = server_info[0]
|
101
|
-
@current_players = server_info[1]
|
102
|
-
@max_players = server_info[2]
|
103
|
-
@online = true
|
104
|
-
end
|
105
|
-
end
|
106
|
-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
107
|
-
return Retval::CONNFAIL
|
108
158
|
rescue Timeout::Error
|
109
159
|
return Retval::TIMEOUT
|
110
160
|
rescue => exception
|
111
161
|
$stderr.puts exception
|
112
162
|
return Retval::UNKNOWN
|
113
163
|
end
|
114
|
-
return
|
164
|
+
return retval
|
115
165
|
end
|
116
166
|
|
117
167
|
# 1.4/1.5
|
@@ -127,49 +177,24 @@ class MineStat
|
|
127
177
|
# server version, message of the day, current players, and max players
|
128
178
|
# The protocol version corresponds with the server version and can be the
|
129
179
|
# same for different server versions.
|
130
|
-
def legacy_query(
|
180
|
+
def legacy_query()
|
181
|
+
retval = nil
|
131
182
|
begin
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
@
|
137
|
-
server.write("\xFE\x01")
|
138
|
-
|
139
|
-
len = server.read(2).unpack('n').first
|
140
|
-
data = server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
|
141
|
-
server.close
|
142
|
-
else
|
143
|
-
server.close
|
144
|
-
return Retval::UNKNOWN
|
145
|
-
end
|
183
|
+
Timeout::timeout(@timeout) do
|
184
|
+
retval = connect()
|
185
|
+
return retval unless retval == Retval::SUCCESS
|
186
|
+
# Perform handshake and acquire data
|
187
|
+
@query_type = "1.4/1.5"
|
188
|
+
@server.write("\xFE\x01")
|
189
|
+
retval = parse_data("\x00") # null
|
146
190
|
end
|
147
|
-
|
148
|
-
if data == nil || data.empty?
|
149
|
-
return Retval::UNKNOWN
|
150
|
-
else
|
151
|
-
server_info = data.split("\x00") # null
|
152
|
-
if server_info != nil && server_info.length >= NUM_FIELDS
|
153
|
-
# server_info[0] contains the section symbol and 1
|
154
|
-
# server_info[1] contains the protocol version (51 for example)
|
155
|
-
@version = server_info[2]
|
156
|
-
@motd = server_info[3]
|
157
|
-
@current_players = server_info[4]
|
158
|
-
@max_players = server_info[5]
|
159
|
-
@online = true
|
160
|
-
else
|
161
|
-
return Retval::UNKNOWN
|
162
|
-
end
|
163
|
-
end
|
164
|
-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
165
|
-
return Retval::CONNFAIL
|
166
191
|
rescue Timeout::Error
|
167
192
|
return Retval::TIMEOUT
|
168
193
|
rescue => exception
|
169
194
|
$stderr.puts exception
|
170
195
|
return Retval::UNKNOWN
|
171
196
|
end
|
172
|
-
return
|
197
|
+
return retval
|
173
198
|
end
|
174
199
|
|
175
200
|
# 1.6
|
@@ -193,56 +218,31 @@ class MineStat
|
|
193
218
|
# server version, message of the day, current players, and max players
|
194
219
|
# The protocol version corresponds with the server version and can be the
|
195
220
|
# same for different server versions.
|
196
|
-
def new_query(
|
221
|
+
def new_query()
|
222
|
+
retval = nil
|
197
223
|
begin
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
@
|
203
|
-
server.write("\xFE\x01\xFA")
|
204
|
-
server.write("\x00\x0B") # 11 (length of "MC|PingHost")
|
205
|
-
server.write('MC|PingHost'.encode('UTF-16BE').force_encoding('ASCII-8BIT'))
|
206
|
-
server.write([7 + 2 * address.length].pack('n'))
|
207
|
-
server.write("\x4E") # 78 (protocol version of 1.6.4)
|
208
|
-
server.write([address.length].pack('n'))
|
209
|
-
server.write(address.encode('UTF-16BE').force_encoding('ASCII-8BIT'))
|
210
|
-
server.write([port].pack('N'))
|
211
|
-
|
212
|
-
len = server.read(2).unpack('n').first
|
213
|
-
data = server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
|
214
|
-
server.close
|
215
|
-
else
|
216
|
-
server.close
|
217
|
-
return Retval::UNKNOWN
|
218
|
-
end
|
224
|
+
Timeout::timeout(@timeout) do
|
225
|
+
retval = connect()
|
226
|
+
return retval unless retval == Retval::SUCCESS
|
227
|
+
# Perform handshake and acquire data
|
228
|
+
@query_type = "1.6"
|
229
|
+
@server.write("\xFE\x01\xFA")
|
230
|
+
@server.write("\x00\x0B") # 11 (length of "MC|PingHost")
|
231
|
+
@server.write('MC|PingHost'.encode('UTF-16BE').force_encoding('ASCII-8BIT'))
|
232
|
+
@server.write([7 + 2 * @address.length].pack('n'))
|
233
|
+
@server.write("\x4E") # 78 (protocol version of 1.6.4)
|
234
|
+
@server.write([@address.length].pack('n'))
|
235
|
+
@server.write(@address.encode('UTF-16BE').force_encoding('ASCII-8BIT'))
|
236
|
+
@server.write([@port].pack('N'))
|
237
|
+
retval = parse_data("\x00") # null
|
219
238
|
end
|
220
|
-
|
221
|
-
if data == nil || data.empty?
|
222
|
-
return Retval::UNKNOWN
|
223
|
-
else
|
224
|
-
server_info = data.split("\x00") # null
|
225
|
-
if server_info != nil && server_info.length >= NUM_FIELDS
|
226
|
-
# server_info[0] contains the section symbol and 1
|
227
|
-
# server_info[1] contains the protocol version (78 for example)
|
228
|
-
@version = server_info[2]
|
229
|
-
@motd = server_info[3]
|
230
|
-
@current_players = server_info[4]
|
231
|
-
@max_players = server_info[5]
|
232
|
-
@online = true
|
233
|
-
else
|
234
|
-
return Retval::UNKNOWN
|
235
|
-
end
|
236
|
-
end
|
237
|
-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
238
|
-
return Retval::CONNFAIL
|
239
239
|
rescue Timeout::Error
|
240
240
|
return Retval::TIMEOUT
|
241
241
|
rescue => exception
|
242
242
|
$stderr.puts exception
|
243
243
|
return Retval::UNKNOWN
|
244
244
|
end
|
245
|
-
return
|
245
|
+
return retval
|
246
246
|
end
|
247
247
|
|
248
248
|
# 1.7
|
@@ -261,9 +261,86 @@ class MineStat
|
|
261
261
|
# {'players': {'max': 20, 'online': 0},
|
262
262
|
# 'version': {'protocol': 404, 'name': '1.13.2'},
|
263
263
|
# 'description': {'text': 'A Minecraft Server'}}
|
264
|
-
def json_query(
|
265
|
-
|
264
|
+
def json_query()
|
265
|
+
retval = nil
|
266
|
+
begin
|
267
|
+
Timeout::timeout(@timeout) do
|
268
|
+
retval = connect()
|
269
|
+
return retval unless retval == Retval::SUCCESS
|
270
|
+
# Perform handshake
|
271
|
+
@query_type = "1.7"
|
272
|
+
payload = "\x00\x00"
|
273
|
+
payload += [@address.length].pack('c') << @address
|
274
|
+
payload += [@port].pack('n')
|
275
|
+
payload += "\x01"
|
276
|
+
payload = [payload.length].pack('c') << payload
|
277
|
+
@server.write(payload)
|
278
|
+
@server.write("\x01\x00")
|
279
|
+
@server.flush
|
280
|
+
|
281
|
+
# Acquire data
|
282
|
+
_total_len = unpack_varint
|
283
|
+
return Retval::UNKNOWN if unpack_varint != 0
|
284
|
+
json_len = unpack_varint
|
285
|
+
json_data = recv_json(json_len)
|
286
|
+
@server.close
|
287
|
+
|
288
|
+
# Parse data
|
289
|
+
json_data = JSON.parse(json_data)
|
290
|
+
@json_data = json_data
|
291
|
+
@protocol = json_data['version']['protocol'].to_i
|
292
|
+
@version = json_data['version']['name']
|
293
|
+
@motd = json_data['description']['text']
|
294
|
+
@current_players = json_data['players']['online'].to_i
|
295
|
+
@max_players = json_data['players']['max'].to_i
|
296
|
+
if !@version.empty? && !@motd.empty? && !@current_players.nil? && !@max_players.nil?
|
297
|
+
@online = true
|
298
|
+
else
|
299
|
+
retval = Retval::UNKNOWN
|
300
|
+
end
|
301
|
+
end
|
302
|
+
rescue Timeout::Error
|
303
|
+
return Retval::TIMEOUT
|
304
|
+
rescue JSON::ParserError
|
305
|
+
return Retval::UNKNOWN
|
306
|
+
rescue => exception
|
307
|
+
$stderr.puts exception
|
308
|
+
return Retval::UNKNOWN
|
309
|
+
end
|
310
|
+
return retval
|
311
|
+
end
|
312
|
+
|
313
|
+
# Reads JSON data from the socket
|
314
|
+
def recv_json(json_len)
|
315
|
+
json_data = ""
|
316
|
+
begin
|
317
|
+
loop do
|
318
|
+
remaining = json_len - json_data.length
|
319
|
+
data = @server.recv(remaining)
|
320
|
+
@server.flush
|
321
|
+
json_data += data
|
322
|
+
break if json_data.length >= json_len
|
323
|
+
end
|
324
|
+
rescue => exception
|
325
|
+
$stderr.puts exception
|
326
|
+
end
|
327
|
+
return json_data
|
328
|
+
end
|
329
|
+
|
330
|
+
# Returns value of varint type
|
331
|
+
def unpack_varint()
|
332
|
+
vint = 0
|
333
|
+
i = 0
|
334
|
+
while i <= MAX_VARINT_SIZE
|
335
|
+
data = @server.read(1)
|
336
|
+
return 0 if data.nil? || data.empty?
|
337
|
+
data = data.ord
|
338
|
+
vint |= (data & 0x7F) << 7 * i
|
339
|
+
break if (data & 0x80) != 128
|
340
|
+
i += 1
|
341
|
+
end
|
342
|
+
return vint
|
266
343
|
end
|
267
344
|
|
268
|
-
attr_reader :address, :port, :online, :version, :motd, :current_players, :max_players, :latency
|
345
|
+
attr_reader :address, :port, :online, :version, :motd, :current_players, :max_players, :protocol, :json_data, :latency, :query_type
|
269
346
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minestat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lloyd Dilley
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-05-17 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: MineStat polls Minecraft server data such as version, motd, current players,
|
15
15
|
and max players using a variety of programming languages.
|