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.
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.