minestat 0.1.1 → 0.2.1

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 +5 -5
  2. data/lib/minestat.rb +230 -28
  3. metadata +10 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: bbf26994bd72901b9d88df8e46c79b40e5640072
4
- data.tar.gz: f62193b7fb9b34246e82b99dcd2afb0d9441596e
2
+ SHA256:
3
+ metadata.gz: 20bcda7c193a48fa96c641630b9a9c732c7b0042d711732c8c6a60774ea425e5
4
+ data.tar.gz: 39fe195c29631131ee3003d0931658556395efa64c1513cbaa28acde0240fea6
5
5
  SHA512:
6
- metadata.gz: 17c83ee983b651a642d42033ee9f7ee5e9841132d8cdd097be845fef9eef70bb009bca2c070fcecd9015cb1cddcad61585799138f0f55eaaef573f7430ce11eb
7
- data.tar.gz: fe11210ae33495c71478e741c4ee86c5f2ca141c1f220e3eb3e1a1befb8f1eaa0189892187414e12cfb180a77aec038f36d065abdc45fab6b2c529b091453d7a
6
+ metadata.gz: 59a25b2226f196216bffe13ba50f3ae4716c9110034bf1db23064fa0ea2b80436fd3e42a996a46a71e7a7041da0edd52767a0df92285b15a54a325545ee4d8f3
7
+ data.tar.gz: 8a18ab5dfecee0d7fc7adeb7b6e63c16165d499db8f679a27ad63833e082fffe52f040b482009b0941750f7eee819bb59ad3f24d8c924d2aef9b339488b83176
@@ -1,5 +1,5 @@
1
1
  # minestat.rb - A Minecraft server status checker
2
- # Copyright (C) 2014, 2016 Lloyd Dilley
2
+ # Copyright (C) 2014 Lloyd Dilley
3
3
  # http://www.dilley.me/
4
4
  #
5
5
  # This program is free software; you can redistribute it and/or modify
@@ -20,48 +20,250 @@ require 'socket'
20
20
  require 'timeout'
21
21
 
22
22
  class MineStat
23
- NUM_FIELDS = 6 # number of values expected from server
23
+ NUM_FIELDS = 6 # number of values expected from server
24
+ NUM_FIELDS_BETA = 3 # number of values expected from a 1.8b/1.3 server
25
+ DEFAULT_TIMEOUT = 5 # default TCP timeout in seconds
24
26
 
25
- def initialize(address, port, timeout = 7)
27
+ module Retval
28
+ SUCCESS = 0
29
+ CONNFAIL = -1
30
+ TIMEOUT = -2
31
+ UNKNOWN = -3
32
+ end
33
+
34
+ def initialize(address, port, timeout = DEFAULT_TIMEOUT)
26
35
  @address = address
27
36
  @port = port
28
- @online # online or offline?
29
- @version # server version
30
- @motd # message of the day
31
- @current_players # current number of players online
32
- @max_players # maximum player capacity
37
+ @online # online or offline?
38
+ @version # server version
39
+ @motd # message of the day
40
+ @current_players # current number of players online
41
+ @max_players # maximum player capacity
42
+ @latency # ping time to server in milliseconds
43
+
44
+ # Try the newest protocol first and work down. If the query succeeds or the
45
+ # connection fails, there is no reason to continue with subsequent queries.
46
+ # Attempts should continue in the event of a timeout however since it may
47
+ # be due to an issue during the handshake.
48
+ # Note: Newer server versions may still respond to older ping query types.
49
+ # For example, 1.13.2 responds to 1.4/1.5 queries, but not 1.6 queries.
50
+ # 1.7
51
+ retval = json_query(address, port, timeout)
52
+ # 1.6
53
+ unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
54
+ retval = new_query(address, port, timeout)
55
+ end
56
+ # 1.4/1.5
57
+ unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
58
+ retval = legacy_query(address, port, timeout)
59
+ end
60
+ # 1.8b/1.3
61
+ unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
62
+ retval = beta_query(address, port, timeout)
63
+ end
64
+
65
+ @online = false unless retval == Retval::SUCCESS
66
+ end
67
+
68
+ # 1.8b/1.3
69
+ # 1.8 beta through 1.3 servers communicate as follows for a ping query:
70
+ # 1. Client sends \xFE (server list ping)
71
+ # 2. Server responds with:
72
+ # 2a. \xFF (kick packet)
73
+ # 2b. data length
74
+ # 2c. 3 fields delimited by \u00A7 (section symbol)
75
+ # The 3 fields, in order, are: message of the day, current players, and max players
76
+ def beta_query(address, port, timeout)
77
+ 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
92
+ 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
+ rescue Timeout::Error
109
+ return Retval::TIMEOUT
110
+ rescue => exception
111
+ $stderr.puts exception
112
+ return Retval::UNKNOWN
113
+ end
114
+ return Retval::SUCCESS
115
+ end
33
116
 
34
- # Connect to the server and get the data
117
+ # 1.4/1.5
118
+ # 1.4 and 1.5 servers communicate as follows for a ping query:
119
+ # 1. Client sends:
120
+ # 1a. \xFE (server list ping)
121
+ # 1b. \x01 (server list ping payload)
122
+ # 2. Server responds with:
123
+ # 2a. \xFF (kick packet)
124
+ # 2b. data length
125
+ # 2c. 6 fields delimited by \x00 (null)
126
+ # The 6 fields, in order, are: the section symbol and 1, protocol version,
127
+ # server version, message of the day, current players, and max players
128
+ # The protocol version corresponds with the server version and can be the
129
+ # same for different server versions.
130
+ def legacy_query(address, port, timeout)
35
131
  begin
36
132
  data = nil
37
133
  Timeout::timeout(timeout) do
134
+ start_time = Time.now
38
135
  server = TCPSocket.new(address, port)
136
+ @latency = ((Time.now - start_time) * 1000).round
39
137
  server.write("\xFE\x01")
40
- data=server.gets()
41
- server.close()
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
42
146
  end
43
- rescue Errno::ECONNREFUSED => e
44
- @online = false
45
- rescue => e # timeout is handled here
46
- @online = false
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
+ rescue Timeout::Error
167
+ return Retval::TIMEOUT
168
+ rescue => exception
169
+ $stderr.puts exception
170
+ return Retval::UNKNOWN
47
171
  end
172
+ return Retval::SUCCESS
173
+ end
48
174
 
49
- # Parse the received data
50
- if data == nil || data.empty?
51
- @online = false
52
- else
53
- server_info = data.split("\x00\x00\x00")
54
- if server_info != nil && server_info.length >= NUM_FIELDS
55
- @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",'')
175
+ # 1.6
176
+ # 1.6 servers communicate as follows for a ping query:
177
+ # 1. Client sends:
178
+ # 1a. \xFE (server list ping)
179
+ # 1b. \x01 (server list ping payload)
180
+ # 1c. \xFA (plugin message)
181
+ # 1d. \x00\x0B (11 which is the length of "MC|PingHost")
182
+ # 1e. "MC|PingHost" encoded as a UTF-16BE string
183
+ # 1f. length of remaining data as a short: remote address (encoded as UTF-16BE) + 7
184
+ # 1g. arbitrary 1.6 protocol version (\x4E for example for 78)
185
+ # 1h. length of remote address as a short
186
+ # 1i. remote address encoded as a UTF-16BE string
187
+ # 1j. remote port as an int
188
+ # 2. Server responds with:
189
+ # 2a. \xFF (kick packet)
190
+ # 2b. data length
191
+ # 2c. 6 fields delimited by \x00 (null)
192
+ # The 6 fields, in order, are: the section symbol and 1, protocol version,
193
+ # server version, message of the day, current players, and max players
194
+ # The protocol version corresponds with the server version and can be the
195
+ # same for different server versions.
196
+ def new_query(address, port, timeout)
197
+ 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
219
+ end
220
+
221
+ if data == nil || data.empty?
222
+ return Retval::UNKNOWN
60
223
  else
61
- @online = false
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
62
236
  end
237
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
238
+ return Retval::CONNFAIL
239
+ rescue Timeout::Error
240
+ return Retval::TIMEOUT
241
+ rescue => exception
242
+ $stderr.puts exception
243
+ return Retval::UNKNOWN
63
244
  end
245
+ return Retval::SUCCESS
246
+ end
247
+
248
+ # 1.7
249
+ # 1.7 to current servers communicate as follows for a ping query:
250
+ # 1. Client sends:
251
+ # 1a. \x00 (handshake packet containing the fields specified below)
252
+ # 1b. \x00 (request)
253
+ # The handshake packet contains the following fields respectively:
254
+ # 1. protocol version as a varint (\x00 suffices)
255
+ # 2. remote address as a string
256
+ # 3. remote port as an unsigned short
257
+ # 4. state as a varint (should be 1 for status)
258
+ # 2. Server responds with:
259
+ # 2a. \x00 (JSON response)
260
+ # An example JSON string contains:
261
+ # {'players': {'max': 20, 'online': 0},
262
+ # 'version': {'protocol': 404, 'name': '1.13.2'},
263
+ # 'description': {'text': 'A Minecraft Server'}}
264
+ def json_query(address, port, timeout)
265
+ return Retval::UNKNOWN # ToDo: Implement me!
64
266
  end
65
267
 
66
- attr_reader :address, :port, :online, :version, :motd, :current_players, :max_players
268
+ attr_reader :address, :port, :online, :version, :motd, :current_players, :max_players, :latency
67
269
  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.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lloyd Dilley
@@ -9,17 +9,20 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-12-28 00:00:00.000000000 Z
12
+ date: 2020-04-26 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description:
15
- email:
14
+ description: MineStat polls Minecraft server data such as version, motd, current players,
15
+ and max players using a variety of programming languages.
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/ldilley/minestat
22
- licenses: []
23
+ homepage: https://github.com/FragLand/minestat
24
+ licenses:
25
+ - GPL-3.0
23
26
  metadata: {}
24
27
  post_install_message:
25
28
  rdoc_options: []
@@ -37,7 +40,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
37
40
  version: '0'
38
41
  requirements: []
39
42
  rubyforge_project:
40
- rubygems_version: 2.6.8
43
+ rubygems_version: 2.7.6.2
41
44
  signing_key:
42
45
  specification_version: 4
43
46
  summary: Minecraft server status checker