minestat 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|