minestat 0.1.1 → 0.2.1

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