minestat 2.1.0 → 2.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.
- checksums.yaml +5 -5
- data/lib/minestat.rb +250 -55
- metadata +7 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e0acf4a379eb97f4145d734b8f7d3b82b9b5f57d
|
4
|
+
data.tar.gz: 8fe0222fc8892a44eea98917e87eaad75f0fd8d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 071fd966c47fd93d01087a4c85d20ca2ca56c05cec1d22c8aeff824602c900f085d7c877e37b0dfe25d3105f74dab2a52d2fa1f19b8706d3a31d16bbb96a7af8
|
7
|
+
data.tar.gz: 4846ca755878fd8d81bddd5634916e83955b142ea587cb81c3ba55a629f04129a2eacdc11f7fa68d9b6d77f5ab8ed141628d330a7e33a5e2fc82e46d61b625ab
|
data/lib/minestat.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# minestat.rb - A Minecraft server status checker
|
2
|
-
# Copyright (C) 2014-
|
2
|
+
# Copyright (C) 2014-2022 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,44 +20,84 @@ require 'json'
|
|
20
20
|
require 'socket'
|
21
21
|
require 'timeout'
|
22
22
|
|
23
|
+
##
|
24
|
+
# Provides a ruby interface for polling Minecraft server status.
|
23
25
|
class MineStat
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
# MineStat version
|
27
|
+
VERSION = "2.2.1"
|
28
|
+
# Number of values expected from server
|
29
|
+
NUM_FIELDS = 6
|
30
|
+
# Number of values expected from a 1.8b/1.3 server
|
31
|
+
NUM_FIELDS_BETA = 3
|
32
|
+
# Maximum number of bytes a varint can be
|
33
|
+
MAX_VARINT_SIZE = 5
|
34
|
+
# Default TCP port
|
35
|
+
DEFAULT_TCP_PORT = 25565
|
36
|
+
# Bedrock/Pocket Edition default UDP port
|
37
|
+
DEFAULT_BEDROCK_PORT = 19132
|
38
|
+
# Default TCP/UDP timeout in seconds
|
39
|
+
DEFAULT_TIMEOUT = 5
|
40
|
+
# Bedrock/Pocket Edition packet offset in bytes (1 + 8 + 8 + 16 + 2)
|
41
|
+
# Unconnected pong (0x1C) = 1 byte
|
42
|
+
# Timestamp as a long = 8 bytes
|
43
|
+
# Server GUID as a long = 8 bytes
|
44
|
+
# Magic number = 16 bytes
|
45
|
+
# String ID length = 2 bytes
|
46
|
+
BEDROCK_PACKET_OFFSET = 35
|
30
47
|
|
48
|
+
##
|
49
|
+
# Stores constants that represent the results of a server ping
|
31
50
|
module Retval
|
51
|
+
# The server ping completed successfully
|
32
52
|
SUCCESS = 0
|
53
|
+
# The server ping failed due to a connection error
|
33
54
|
CONNFAIL = -1
|
55
|
+
# The server ping failed due to a connection time out
|
34
56
|
TIMEOUT = -2
|
57
|
+
# The server ping failed for an unknown reason
|
35
58
|
UNKNOWN = -3
|
36
59
|
end
|
37
60
|
|
61
|
+
##
|
62
|
+
# Stores constants that represent the different kinds of server
|
63
|
+
# list pings/requests that a Minecraft server might expect when
|
64
|
+
# being polled for status information.
|
38
65
|
module Request
|
66
|
+
# Try everything
|
39
67
|
NONE = -1
|
68
|
+
# Server versions 1.8b to 1.3
|
40
69
|
BETA = 0
|
70
|
+
# Server versions 1.4 to 1.5
|
41
71
|
LEGACY = 1
|
72
|
+
# Server version 1.6
|
42
73
|
EXTENDED = 2
|
74
|
+
# Server versions 1.7 to latest
|
43
75
|
JSON = 3
|
76
|
+
# Bedrock/Pocket Edition
|
77
|
+
BEDROCK = 4
|
44
78
|
end
|
45
79
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
@
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
53
|
-
@
|
54
|
-
@
|
55
|
-
@
|
56
|
-
@
|
57
|
-
@
|
58
|
-
@
|
59
|
-
@
|
60
|
-
@
|
80
|
+
##
|
81
|
+
# Instantiate an instance of MineStat and poll the specified server for information
|
82
|
+
def initialize(address, port = DEFAULT_TCP_PORT, timeout = DEFAULT_TIMEOUT, request_type = Request::NONE)
|
83
|
+
@address = address # address of server
|
84
|
+
@port = port # TCP/UDP port of server
|
85
|
+
@online # online or offline?
|
86
|
+
@version # server version
|
87
|
+
@mode = "Unspecified" # game mode (Bedrock/Pocket Edition only)
|
88
|
+
@motd # message of the day
|
89
|
+
@stripped_motd # message of the day without formatting
|
90
|
+
@current_players # current number of players online
|
91
|
+
@max_players # maximum player capacity
|
92
|
+
@protocol # protocol level
|
93
|
+
@json_data # JSON data for 1.7 queries
|
94
|
+
@latency # ping time to server in milliseconds
|
95
|
+
@timeout = timeout # TCP/UDP timeout
|
96
|
+
@server # server socket
|
97
|
+
@request_type # Protocol version
|
98
|
+
@try_all = false # try all protocols?
|
99
|
+
|
100
|
+
@try_all = true if request_type == Request::NONE
|
61
101
|
|
62
102
|
case request_type
|
63
103
|
when Request::BETA
|
@@ -68,13 +108,14 @@ class MineStat
|
|
68
108
|
retval = extended_legacy_request()
|
69
109
|
when Request::JSON
|
70
110
|
retval = json_request()
|
111
|
+
when Request::BEDROCK
|
112
|
+
retval = bedrock_request()
|
71
113
|
else
|
72
|
-
# Attempt various
|
73
|
-
#
|
74
|
-
#
|
114
|
+
# Attempt various ping requests in a particular order. If the
|
115
|
+
# connection fails, there is no reason to continue with subsequent
|
116
|
+
# requests. Attempts should continue in the event of a timeout
|
75
117
|
# however since it may be due to an issue during the handshake.
|
76
118
|
# Note: Newer server versions may still respond to older SLP requests.
|
77
|
-
# For example, 1.13.2 responds to 1.4/1.5 queries, but not 1.6 queries.
|
78
119
|
# SLP 1.4/1.5
|
79
120
|
retval = legacy_request()
|
80
121
|
# SLP 1.8b/1.3
|
@@ -89,31 +130,46 @@ class MineStat
|
|
89
130
|
unless retval == Retval::CONNFAIL
|
90
131
|
retval = json_request()
|
91
132
|
end
|
133
|
+
# Bedrock/Pocket Edition
|
134
|
+
unless retval == Retval::CONNFAIL
|
135
|
+
retval = bedrock_request()
|
136
|
+
end
|
92
137
|
end
|
93
138
|
@online = false unless retval == Retval::SUCCESS
|
94
139
|
end
|
95
140
|
|
96
141
|
# Strips message of the day formatting characters
|
97
|
-
def strip_motd(
|
98
|
-
unless
|
99
|
-
@stripped_motd = @motd.gsub(/§./, "")
|
100
|
-
else
|
142
|
+
def strip_motd()
|
143
|
+
unless @motd['text'] == nil
|
101
144
|
@stripped_motd = @motd['text']
|
145
|
+
else
|
146
|
+
@stripped_motd = @motd
|
147
|
+
end
|
148
|
+
unless @motd['extra'] == nil
|
102
149
|
json_data = @motd['extra']
|
103
150
|
unless json_data.nil? || json_data.empty?
|
104
151
|
json_data.each do |nested_hash|
|
105
152
|
@stripped_motd += nested_hash['text']
|
106
153
|
end
|
107
154
|
end
|
108
|
-
@stripped_motd = @stripped_motd.gsub(/§./, "")
|
109
155
|
end
|
156
|
+
@stripped_motd = @stripped_motd.force_encoding('UTF-8')
|
157
|
+
@stripped_motd = @stripped_motd.gsub(/§./, "")
|
110
158
|
end
|
111
159
|
|
112
|
-
|
160
|
+
##
|
161
|
+
# Establishes a connection to the Minecraft server
|
113
162
|
def connect()
|
114
163
|
begin
|
115
|
-
|
116
|
-
|
164
|
+
if @request_type == Request::BEDROCK || @request_type == "Bedrock/Pocket Edition"
|
165
|
+
@port = DEFAULT_BEDROCK_PORT if @port == DEFAULT_TCP_PORT && @try_all
|
166
|
+
start_time = Time.now
|
167
|
+
@server = UDPSocket.new
|
168
|
+
@server.connect(@address, @port)
|
169
|
+
else
|
170
|
+
start_time = Time.now
|
171
|
+
@server = TCPSocket.new(@address, @port)
|
172
|
+
end
|
117
173
|
@latency = ((Time.now - start_time) * 1000).round
|
118
174
|
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
119
175
|
return Retval::CONNFAIL
|
@@ -128,16 +184,28 @@ class MineStat
|
|
128
184
|
def parse_data(delimiter, is_beta = false)
|
129
185
|
data = nil
|
130
186
|
begin
|
131
|
-
if @
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
187
|
+
if @request_type == "Bedrock/Pocket Edition"
|
188
|
+
if @server.recv(1, Socket::MSG_PEEK).unpack('C').first == 0x1C # unconnected pong packet
|
189
|
+
server_id_len = @server.recv(BEDROCK_PACKET_OFFSET, Socket::MSG_PEEK)[-2..-1].unpack('n').first
|
190
|
+
data = @server.recv(BEDROCK_PACKET_OFFSET + server_id_len)[BEDROCK_PACKET_OFFSET..-1]
|
191
|
+
@server.close
|
192
|
+
else
|
193
|
+
@server.close
|
194
|
+
return Retval::UNKNOWN
|
195
|
+
end
|
196
|
+
else # SLP
|
197
|
+
if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
|
198
|
+
len = @server.read(2).unpack('n').first
|
199
|
+
data = @server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
|
200
|
+
@server.close
|
201
|
+
else
|
202
|
+
@server.close
|
203
|
+
return Retval::UNKNOWN
|
204
|
+
end
|
138
205
|
end
|
139
|
-
rescue
|
140
|
-
|
206
|
+
rescue
|
207
|
+
#rescue => exception
|
208
|
+
#$stderr.puts exception
|
141
209
|
return Retval::UNKNOWN
|
142
210
|
end
|
143
211
|
|
@@ -157,7 +225,20 @@ class MineStat
|
|
157
225
|
else
|
158
226
|
return Retval::UNKNOWN
|
159
227
|
end
|
160
|
-
|
228
|
+
elsif @request_type == "Bedrock/Pocket Edition"
|
229
|
+
if server_info != nil
|
230
|
+
@protocol = server_info[2].to_i
|
231
|
+
@version = "#{server_info[3]} #{server_info[7]} (#{server_info[0]})"
|
232
|
+
@mode = server_info[8]
|
233
|
+
@motd = server_info[1]
|
234
|
+
strip_motd
|
235
|
+
@current_players = server_info[4].to_i
|
236
|
+
@max_players = server_info[5].to_i
|
237
|
+
@online = true
|
238
|
+
else
|
239
|
+
return Retval::UNKNOWN
|
240
|
+
end
|
241
|
+
else # SLP
|
161
242
|
if server_info != nil && server_info.length >= NUM_FIELDS
|
162
243
|
# server_info[0] contains the section symbol and 1
|
163
244
|
@protocol = server_info[1].to_i # contains the protocol version (51 for 1.9 or 78 for 1.6.4 for example)
|
@@ -174,14 +255,17 @@ class MineStat
|
|
174
255
|
return Retval::SUCCESS
|
175
256
|
end
|
176
257
|
|
177
|
-
|
258
|
+
##
|
178
259
|
# 1.8 beta through 1.3 servers communicate as follows for a ping request:
|
179
260
|
# 1. Client sends \xFE (server list ping)
|
180
261
|
# 2. Server responds with:
|
181
262
|
# 2a. \xFF (kick packet)
|
182
263
|
# 2b. data length
|
183
264
|
# 2c. 3 fields delimited by \u00A7 (section symbol)
|
184
|
-
# The 3 fields, in order, are:
|
265
|
+
# The 3 fields, in order, are:
|
266
|
+
# * message of the day
|
267
|
+
# * current players
|
268
|
+
# * max players
|
185
269
|
def beta_request()
|
186
270
|
retval = nil
|
187
271
|
begin
|
@@ -202,7 +286,7 @@ class MineStat
|
|
202
286
|
return retval
|
203
287
|
end
|
204
288
|
|
205
|
-
|
289
|
+
##
|
206
290
|
# 1.4 and 1.5 servers communicate as follows for a ping request:
|
207
291
|
# 1. Client sends:
|
208
292
|
# 1a. \xFE (server list ping)
|
@@ -211,8 +295,14 @@ class MineStat
|
|
211
295
|
# 2a. \xFF (kick packet)
|
212
296
|
# 2b. data length
|
213
297
|
# 2c. 6 fields delimited by \x00 (null)
|
214
|
-
# The 6 fields, in order, are:
|
215
|
-
#
|
298
|
+
# The 6 fields, in order, are:
|
299
|
+
# * the section symbol and 1
|
300
|
+
# * protocol version
|
301
|
+
# * server version
|
302
|
+
# * message of the day
|
303
|
+
# * current players
|
304
|
+
# * max players
|
305
|
+
#
|
216
306
|
# The protocol version corresponds with the server version and can be the
|
217
307
|
# same for different server versions.
|
218
308
|
def legacy_request()
|
@@ -235,7 +325,7 @@ class MineStat
|
|
235
325
|
return retval
|
236
326
|
end
|
237
327
|
|
238
|
-
|
328
|
+
##
|
239
329
|
# 1.6 servers communicate as follows for a ping request:
|
240
330
|
# 1. Client sends:
|
241
331
|
# 1a. \xFE (server list ping)
|
@@ -252,8 +342,14 @@ class MineStat
|
|
252
342
|
# 2a. \xFF (kick packet)
|
253
343
|
# 2b. data length
|
254
344
|
# 2c. 6 fields delimited by \x00 (null)
|
255
|
-
# The 6 fields, in order, are:
|
256
|
-
#
|
345
|
+
# The 6 fields, in order, are:
|
346
|
+
# * the section symbol and 1
|
347
|
+
# * protocol version
|
348
|
+
# * server version
|
349
|
+
# * message of the day
|
350
|
+
# * current players
|
351
|
+
# * max players
|
352
|
+
#
|
257
353
|
# The protocol version corresponds with the server version and can be the
|
258
354
|
# same for different server versions.
|
259
355
|
def extended_legacy_request()
|
@@ -283,7 +379,7 @@ class MineStat
|
|
283
379
|
return retval
|
284
380
|
end
|
285
381
|
|
286
|
-
|
382
|
+
##
|
287
383
|
# 1.7 to current servers communicate as follows for a ping request:
|
288
384
|
# 1. Client sends:
|
289
385
|
# 1a. \x00 (handshake packet containing the fields specified below)
|
@@ -329,7 +425,7 @@ class MineStat
|
|
329
425
|
@protocol = json_data['version']['protocol'].to_i
|
330
426
|
@version = json_data['version']['name']
|
331
427
|
@motd = json_data['description']
|
332
|
-
strip_motd
|
428
|
+
strip_motd
|
333
429
|
@current_players = json_data['players']['online'].to_i
|
334
430
|
@max_players = json_data['players']['max'].to_i
|
335
431
|
if !@version.empty? && !@motd.empty? && !@current_players.nil? && !@max_players.nil?
|
@@ -381,5 +477,104 @@ class MineStat
|
|
381
477
|
return vint
|
382
478
|
end
|
383
479
|
|
384
|
-
|
480
|
+
##
|
481
|
+
# Bedrock/Pocket Edition servers communicate as follows for an unconnected ping request:
|
482
|
+
# 1. Client sends:
|
483
|
+
# 1a. \x01 (unconnected ping packet containing the fields specified below)
|
484
|
+
# 1b. current time as a long
|
485
|
+
# 1c. magic number
|
486
|
+
# 1d. client GUID as a long
|
487
|
+
# 2. Server responds with:
|
488
|
+
# 2a. \x1c (unconnected pong packet containing the follow fields)
|
489
|
+
# 2b. current time as a long
|
490
|
+
# 2c. server GUID as a long
|
491
|
+
# 2d. 16-bit magic number
|
492
|
+
# 2e. server ID string length
|
493
|
+
# 2f. server ID as a string
|
494
|
+
# The fields from the pong response, in order, are:
|
495
|
+
# * edition
|
496
|
+
# * MotD line 1
|
497
|
+
# * protocol version
|
498
|
+
# * version name
|
499
|
+
# * current player count
|
500
|
+
# * maximum player count
|
501
|
+
# * unique server ID
|
502
|
+
# * MotD line 2
|
503
|
+
# * game mode as a string
|
504
|
+
# * game mode as a numeric
|
505
|
+
# * IPv4 port number
|
506
|
+
# * IPv6 port number
|
507
|
+
def bedrock_request()
|
508
|
+
retval = nil
|
509
|
+
begin
|
510
|
+
Timeout::timeout(@timeout) do
|
511
|
+
@request_type = "Bedrock/Pocket Edition"
|
512
|
+
retval = connect()
|
513
|
+
return retval unless retval == Retval::SUCCESS
|
514
|
+
# Perform handshake and acquire data
|
515
|
+
payload = "\x01".force_encoding('ASCII-8BIT') # unconnected ping
|
516
|
+
payload += [Time.now.to_i].pack('L!<').force_encoding('ASCII-8BIT') # current time as a long
|
517
|
+
payload += "\x00\xFF\xFF\x00\xFE\xFE\xFE\xFE\xFD\xFD\xFD\xFD\x12\x34\x56\x78".force_encoding('ASCII-8BIT') # magic number
|
518
|
+
payload += [2].pack('L!<').force_encoding('ASCII-8BIT') # client GUID as a long
|
519
|
+
@server.write(payload)
|
520
|
+
@server.flush
|
521
|
+
retval = parse_data("\x3B") # semicolon
|
522
|
+
end
|
523
|
+
rescue Timeout::Error
|
524
|
+
return Retval::TIMEOUT
|
525
|
+
rescue => exception
|
526
|
+
$stderr.puts exception
|
527
|
+
return Retval::UNKNOWN
|
528
|
+
end
|
529
|
+
return retval
|
530
|
+
end
|
531
|
+
|
532
|
+
# Returns the Minecraft server IP
|
533
|
+
attr_reader :address
|
534
|
+
|
535
|
+
# Returns the Minecraft server TCP port
|
536
|
+
attr_reader :port
|
537
|
+
|
538
|
+
# Returns a boolean describing whether the server is online or offline
|
539
|
+
attr_reader :online
|
540
|
+
|
541
|
+
# Returns the Minecraft version that the server is running
|
542
|
+
attr_reader :version
|
543
|
+
|
544
|
+
# Returns the game mode (Bedrock/Pocket Edition only)
|
545
|
+
attr_reader :mode
|
546
|
+
|
547
|
+
# Returns the full version of the MotD
|
548
|
+
#
|
549
|
+
# If you just want the MotD text, use stripped_motd
|
550
|
+
attr_reader :motd
|
551
|
+
|
552
|
+
# Returns just the plain text contained within the MotD
|
553
|
+
attr_reader :stripped_motd
|
554
|
+
|
555
|
+
# Returns the current player count
|
556
|
+
attr_reader :current_players
|
557
|
+
|
558
|
+
# Returns the maximum player count
|
559
|
+
attr_reader :max_players
|
560
|
+
|
561
|
+
# Returns the protocol level
|
562
|
+
#
|
563
|
+
# This is arbitrary and varies by Minecraft version.
|
564
|
+
# However, multiple Minecraft versions can share the same
|
565
|
+
# protocol level
|
566
|
+
attr_reader :protocol
|
567
|
+
|
568
|
+
# Returns the complete JSON response data for queries to Minecraft
|
569
|
+
# servers with a version greater than or equal to 1.7
|
570
|
+
attr_reader :json_data
|
571
|
+
|
572
|
+
# Returns the ping time to the server in ms
|
573
|
+
attr_reader :latency
|
574
|
+
|
575
|
+
# Returns the protocol version
|
576
|
+
attr_reader :request_type
|
577
|
+
|
578
|
+
# Returns whether or not all ping protocols should be attempted
|
579
|
+
attr_reader :try_all
|
385
580
|
end
|
metadata
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minestat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lloyd Dilley
|
8
|
-
|
9
|
-
autorequire:
|
8
|
+
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2022-05-15 00:00:00.000000000 Z
|
13
12
|
dependencies: []
|
14
13
|
description: MineStat polls Minecraft server data such as version, motd, current players,
|
15
14
|
and max players.
|
@@ -24,7 +23,7 @@ homepage: https://github.com/FragLand/minestat
|
|
24
23
|
licenses:
|
25
24
|
- GPL-3.0
|
26
25
|
metadata: {}
|
27
|
-
post_install_message:
|
26
|
+
post_install_message:
|
28
27
|
rdoc_options: []
|
29
28
|
require_paths:
|
30
29
|
- lib
|
@@ -39,9 +38,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
38
|
- !ruby/object:Gem::Version
|
40
39
|
version: '0'
|
41
40
|
requirements: []
|
42
|
-
rubyforge_project:
|
43
|
-
rubygems_version: 2.
|
44
|
-
signing_key:
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 2.5.2.1
|
43
|
+
signing_key:
|
45
44
|
specification_version: 4
|
46
45
|
summary: Minecraft server status checker
|
47
46
|
test_files: []
|