minestat 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/minestat.rb +195 -118
  3. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20bcda7c193a48fa96c641630b9a9c732c7b0042d711732c8c6a60774ea425e5
4
- data.tar.gz: 39fe195c29631131ee3003d0931658556395efa64c1513cbaa28acde0240fea6
3
+ metadata.gz: ca86c11c59beb2c8fb7eefa8e4a6730b9463b6abdab1241201ba897b5a3e2eee
4
+ data.tar.gz: 51ae47ef99e1a524617d617ffb9c602f5a1673c4b8d6326f4e122e27d054c242
5
5
  SHA512:
6
- metadata.gz: 59a25b2226f196216bffe13ba50f3ae4716c9110034bf1db23064fa0ea2b80436fd3e42a996a46a71e7a7041da0edd52767a0df92285b15a54a325545ee4d8f3
7
- data.tar.gz: 8a18ab5dfecee0d7fc7adeb7b6e63c16165d499db8f679a27ad63833e082fffe52f040b482009b0941750f7eee819bb59ad3f24d8c924d2aef9b339488b83176
6
+ metadata.gz: 5f84a7ec58ae1382231820b26fcefc146bd71e31b35ef2e3d216836d70f04130c0e23b87e20e1c04f4e701b536335a53e5f6c7d0fad1c7bb89772a6f1d935609
7
+ data.tar.gz: 37a5c9628166579b945d3961c20cbae585964d981cb293bb03f4fe5222e148bd547a1a10044c58b007870210649a65f122aa5a96bcc0ca911fbe515d17d9cd07
@@ -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(address, port, timeout)
59
+ retval = json_query()
52
60
  # 1.6
53
61
  unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
54
- retval = new_query(address, port, timeout)
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(address, port, timeout)
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(address, port, timeout)
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(address, port, timeout)
147
+ def beta_query()
148
+ retval = nil
77
149
  begin
78
- data = nil
79
- Timeout::timeout(timeout) do
80
- start_time = Time.now
81
- server = TCPSocket.new(address, port)
82
- @latency = ((Time.now - start_time) * 1000).round
83
- server.write("\xFE")
84
- if server.read(1).unpack('C').first == 0xFF # kick packet (255)
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 Retval::SUCCESS
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(address, port, timeout)
180
+ def legacy_query()
181
+ retval = nil
131
182
  begin
132
- data = nil
133
- Timeout::timeout(timeout) do
134
- start_time = Time.now
135
- server = TCPSocket.new(address, port)
136
- @latency = ((Time.now - start_time) * 1000).round
137
- server.write("\xFE\x01")
138
- if server.read(1).unpack('C').first == 0xFF # kick packet (255)
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 Retval::SUCCESS
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(address, port, timeout)
221
+ def new_query()
222
+ retval = nil
197
223
  begin
198
- data = nil
199
- Timeout::timeout(DEFAULT_TIMEOUT) do
200
- start_time = Time.now
201
- server = TCPSocket.new(address, port)
202
- @latency = ((Time.now - start_time) * 1000).round
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
- if server.read(1).unpack('C').first == 0xFF # kick packet (255)
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 Retval::SUCCESS
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(address, port, timeout)
265
- return Retval::UNKNOWN # ToDo: Implement me!
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.2.1
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-04-26 00:00:00.000000000 Z
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.