minestat 2.0.0 → 2.2.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 +258 -50
  3. metadata +6 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c241df5d1adebf63bf3fa8ff85a4066cf788b64ac160afddf99906f6d0ea1a3
4
- data.tar.gz: c257c9b9640bb4bf961bdc80dfbebbcad829cfe5a56f90951bc724859ab389c7
3
+ metadata.gz: 289592d2813eb1246b9a3033ce10a219513bf0c9cb792951051bdf6f97b20598
4
+ data.tar.gz: beee739e9c19250fb6663e9561e065dfbcbae817442afe07ca3a21911e5f975a
5
5
  SHA512:
6
- metadata.gz: afb6c96adcac8c9bcea0bb88e1466ef9354d116345707c1f9fa901322996d412c8244a59c5facd724909d7f16375f5c3dcc098f40aef484ca8a9d18dff4936c4
7
- data.tar.gz: c08c4a7f1099ed57094bedf7c4f3eed374eff7fa824be55a1962b3fa3831f6f2f088284ee0b58acc09c7285b33fea8892ed8abfbe5b757264876497c3ff637d0
6
+ metadata.gz: ad094493fac2c5b788c7ed86bdcc1e5296715d410cfe435491101fa56aae2bda6cd81644a88ce8caf183e6ae13072e559a073a4a4b42f9a51b0dfe261a46b378
7
+ data.tar.gz: a757852cab9404fe9937a9a4f9fb7bac2effccd3317173d2abf0bc200a87dea5a6576e382c878be9d15f31b2489298197fafa6f5c1b4eaa1aff75d90f6ab6c16
data/lib/minestat.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # minestat.rb - A Minecraft server status checker
2
- # Copyright (C) 2014-2021 Lloyd Dilley
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,43 +20,81 @@ 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
- VERSION = "2.0.0" # MineStat version
25
- NUM_FIELDS = 6 # number of values expected from server
26
- NUM_FIELDS_BETA = 3 # number of values expected from a 1.8b/1.3 server
27
- MAX_VARINT_SIZE = 5 # maximum number of bytes a varint can be
28
- DEFAULT_PORT = 25565 # default TCP port
29
- DEFAULT_TIMEOUT = 5 # default TCP timeout in seconds
26
+ # MineStat version
27
+ VERSION = "2.2.0"
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_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
 
80
+ ##
81
+ # Instantiate an instance of MineStat and poll the specified server for information
46
82
  def initialize(address, port = DEFAULT_PORT, timeout = DEFAULT_TIMEOUT, request_type = Request::NONE)
47
- @address = address # address of server
48
- @port = port # TCP port of server
49
- @online # online or offline?
50
- @version # server version
51
- @motd # message of the day
52
- @current_players # current number of players online
53
- @max_players # maximum player capacity
54
- @protocol # protocol level
55
- @json_data # JSON data for 1.7 queries
56
- @latency # ping time to server in milliseconds
57
- @timeout = timeout # TCP timeout
58
- @server # server socket
59
- @request_type # SLP protocol version
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
60
98
 
61
99
  case request_type
62
100
  when Request::BETA
@@ -67,13 +105,14 @@ class MineStat
67
105
  retval = extended_legacy_request()
68
106
  when Request::JSON
69
107
  retval = json_request()
108
+ when Request::BEDROCK
109
+ retval = bedrock_request()
70
110
  else
71
- # Attempt various SLP ping requests in a particular order. If the request
72
- # succeeds or the connection fails, there is no reason to continue with
73
- # subsequent requests. Attempts should continue in the event of a timeout
111
+ # Attempt various SLP ping requests in a particular order. If the
112
+ # connection fails, there is no reason to continue with subsequent
113
+ # requests. Attempts should continue in the event of a timeout
74
114
  # however since it may be due to an issue during the handshake.
75
115
  # Note: Newer server versions may still respond to older SLP requests.
76
- # For example, 1.13.2 responds to 1.4/1.5 queries, but not 1.6 queries.
77
116
  # SLP 1.4/1.5
78
117
  retval = legacy_request()
79
118
  # SLP 1.8b/1.3
@@ -81,22 +120,53 @@ class MineStat
81
120
  retval = beta_request()
82
121
  end
83
122
  # SLP 1.6
84
- unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
123
+ unless retval == Retval::CONNFAIL
85
124
  retval = extended_legacy_request()
86
125
  end
87
126
  # SLP 1.7
88
- unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL
127
+ unless retval == Retval::CONNFAIL
89
128
  retval = json_request()
90
129
  end
130
+ # Bedrock
131
+ unless retval == Retval::CONNFAIL
132
+ retval = bedrock_request()
133
+ end
91
134
  end
92
135
  @online = false unless retval == Retval::SUCCESS
93
136
  end
94
137
 
95
- # Connects to remote server
138
+ # Strips message of the day formatting characters
139
+ def strip_motd()
140
+ unless @motd['text'] == nil
141
+ @stripped_motd = @motd['text']
142
+ else
143
+ @stripped_motd = @motd
144
+ end
145
+ unless @motd['extra'] == nil
146
+ json_data = @motd['extra']
147
+ unless json_data.nil? || json_data.empty?
148
+ json_data.each do |nested_hash|
149
+ @stripped_motd += nested_hash['text']
150
+ end
151
+ end
152
+ end
153
+ @stripped_motd = @stripped_motd.force_encoding('UTF-8')
154
+ @stripped_motd = @stripped_motd.gsub(/§./, "")
155
+ end
156
+
157
+ ##
158
+ # Establishes a connection to the Minecraft server
96
159
  def connect()
97
160
  begin
98
- start_time = Time.now
99
- @server = TCPSocket.new(@address, @port)
161
+ if @request_type == Request::BEDROCK || @request_type == "Bedrock/Pocket Edition"
162
+ @port = DEFAULT_BEDROCK_PORT if @port == DEFAULT_PORT && @request_type == Request::NONE
163
+ start_time = Time.now
164
+ @server = UDPSocket.new
165
+ @server.connect(@address, @port)
166
+ else
167
+ start_time = Time.now
168
+ @server = TCPSocket.new(@address, @port)
169
+ end
100
170
  @latency = ((Time.now - start_time) * 1000).round
101
171
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
102
172
  return Retval::CONNFAIL
@@ -111,16 +181,28 @@ class MineStat
111
181
  def parse_data(delimiter, is_beta = false)
112
182
  data = nil
113
183
  begin
114
- if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
115
- len = @server.read(2).unpack('n').first
116
- data = @server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
117
- @server.close
118
- else
119
- @server.close
120
- return Retval::UNKNOWN
184
+ if @request_type == "Bedrock/Pocket Edition"
185
+ if @server.recv(1, Socket::MSG_PEEK).unpack('C').first == 0x1C # unconnected pong packet
186
+ server_id_len = @server.recv(BEDROCK_PACKET_OFFSET, Socket::MSG_PEEK)[-2..-1].unpack('n').first
187
+ data = @server.recv(BEDROCK_PACKET_OFFSET + server_id_len)[BEDROCK_PACKET_OFFSET..-1]
188
+ @server.close
189
+ else
190
+ @server.close
191
+ return Retval::UNKNOWN
192
+ end
193
+ else # SLP
194
+ if @server.read(1).unpack('C').first == 0xFF # kick packet (255)
195
+ len = @server.read(2).unpack('n').first
196
+ data = @server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8')
197
+ @server.close
198
+ else
199
+ @server.close
200
+ return Retval::UNKNOWN
201
+ end
121
202
  end
122
- rescue => exception
123
- $stderr.puts exception
203
+ rescue
204
+ #rescue => exception
205
+ #$stderr.puts exception
124
206
  return Retval::UNKNOWN
125
207
  end
126
208
 
@@ -133,18 +215,33 @@ class MineStat
133
215
  if server_info != nil && server_info.length >= NUM_FIELDS_BETA
134
216
  @version = ">=1.8b/1.3" # since server does not return version, set it
135
217
  @motd = server_info[0]
218
+ strip_motd
136
219
  @current_players = server_info[1].to_i
137
220
  @max_players = server_info[2].to_i
138
221
  @online = true
139
222
  else
140
223
  return Retval::UNKNOWN
141
224
  end
142
- else
225
+ elsif @request_type == "Bedrock/Pocket Edition"
226
+ if server_info != nil
227
+ @protocol = server_info[2].to_i
228
+ @version = "#{server_info[3]} #{server_info[7]} (#{server_info[0]})"
229
+ @mode = server_info[8]
230
+ @motd = server_info[1]
231
+ strip_motd
232
+ @current_players = server_info[4].to_i
233
+ @max_players = server_info[5].to_i
234
+ @online = true
235
+ else
236
+ return Retval::UNKNOWN
237
+ end
238
+ else # SLP
143
239
  if server_info != nil && server_info.length >= NUM_FIELDS
144
240
  # server_info[0] contains the section symbol and 1
145
241
  @protocol = server_info[1].to_i # contains the protocol version (51 for 1.9 or 78 for 1.6.4 for example)
146
242
  @version = server_info[2]
147
243
  @motd = server_info[3]
244
+ strip_motd
148
245
  @current_players = server_info[4].to_i
149
246
  @max_players = server_info[5].to_i
150
247
  @online = true
@@ -155,14 +252,17 @@ class MineStat
155
252
  return Retval::SUCCESS
156
253
  end
157
254
 
158
- # 1.8b/1.3
255
+ ##
159
256
  # 1.8 beta through 1.3 servers communicate as follows for a ping request:
160
257
  # 1. Client sends \xFE (server list ping)
161
258
  # 2. Server responds with:
162
259
  # 2a. \xFF (kick packet)
163
260
  # 2b. data length
164
261
  # 2c. 3 fields delimited by \u00A7 (section symbol)
165
- # The 3 fields, in order, are: message of the day, current players, and max players
262
+ # The 3 fields, in order, are:
263
+ # * message of the day
264
+ # * current players
265
+ # * max players
166
266
  def beta_request()
167
267
  retval = nil
168
268
  begin
@@ -183,7 +283,7 @@ class MineStat
183
283
  return retval
184
284
  end
185
285
 
186
- # 1.4/1.5
286
+ ##
187
287
  # 1.4 and 1.5 servers communicate as follows for a ping request:
188
288
  # 1. Client sends:
189
289
  # 1a. \xFE (server list ping)
@@ -192,8 +292,14 @@ class MineStat
192
292
  # 2a. \xFF (kick packet)
193
293
  # 2b. data length
194
294
  # 2c. 6 fields delimited by \x00 (null)
195
- # The 6 fields, in order, are: the section symbol and 1, protocol version,
196
- # server version, message of the day, current players, and max players
295
+ # The 6 fields, in order, are:
296
+ # * the section symbol and 1
297
+ # * protocol version
298
+ # * server version
299
+ # * message of the day
300
+ # * current players
301
+ # * max players
302
+ #
197
303
  # The protocol version corresponds with the server version and can be the
198
304
  # same for different server versions.
199
305
  def legacy_request()
@@ -216,7 +322,7 @@ class MineStat
216
322
  return retval
217
323
  end
218
324
 
219
- # 1.6
325
+ ##
220
326
  # 1.6 servers communicate as follows for a ping request:
221
327
  # 1. Client sends:
222
328
  # 1a. \xFE (server list ping)
@@ -233,8 +339,14 @@ class MineStat
233
339
  # 2a. \xFF (kick packet)
234
340
  # 2b. data length
235
341
  # 2c. 6 fields delimited by \x00 (null)
236
- # The 6 fields, in order, are: the section symbol and 1, protocol version,
237
- # server version, message of the day, current players, and max players
342
+ # The 6 fields, in order, are:
343
+ # * the section symbol and 1
344
+ # * protocol version
345
+ # * server version
346
+ # * message of the day
347
+ # * current players
348
+ # * max players
349
+ #
238
350
  # The protocol version corresponds with the server version and can be the
239
351
  # same for different server versions.
240
352
  def extended_legacy_request()
@@ -264,7 +376,7 @@ class MineStat
264
376
  return retval
265
377
  end
266
378
 
267
- # 1.7
379
+ ##
268
380
  # 1.7 to current servers communicate as follows for a ping request:
269
381
  # 1. Client sends:
270
382
  # 1a. \x00 (handshake packet containing the fields specified below)
@@ -309,7 +421,8 @@ class MineStat
309
421
  @json_data = json_data
310
422
  @protocol = json_data['version']['protocol'].to_i
311
423
  @version = json_data['version']['name']
312
- @motd = json_data['description']['text']
424
+ @motd = json_data['description']
425
+ strip_motd
313
426
  @current_players = json_data['players']['online'].to_i
314
427
  @max_players = json_data['players']['max'].to_i
315
428
  if !@version.empty? && !@motd.empty? && !@current_players.nil? && !@max_players.nil?
@@ -361,5 +474,100 @@ class MineStat
361
474
  return vint
362
475
  end
363
476
 
364
- attr_reader :address, :port, :online, :version, :motd, :current_players, :max_players, :protocol, :json_data, :latency, :request_type
477
+ ##
478
+ # Bedrock/Pocket Edition servers communicate as follows for an unconnected ping request:
479
+ # 1. Client sends:
480
+ # 1a. \x01 (unconnected ping packet containing the fields specified below)
481
+ # 1b. current time as a long
482
+ # 1c. magic number
483
+ # 1d. client GUID as a long
484
+ # 2. Server responds with:
485
+ # 2a. \x1c (unconnected pong packet containing the follow fields)
486
+ # 2b. current time as a long
487
+ # 2c. server GUID as a long
488
+ # 2d. 16-bit magic number
489
+ # 2e. server ID as a string
490
+ # The fields from the pong response, in order, are:
491
+ # * edition
492
+ # * MotD line 1
493
+ # * protocol version
494
+ # * version name
495
+ # * current player count
496
+ # * maximum player count
497
+ # * unique server ID
498
+ # * MotD line 2
499
+ # * game mode as a string
500
+ # * game mode as a numeric
501
+ # * IPv4 port number
502
+ # * IPv6 port number
503
+ def bedrock_request()
504
+ retval = nil
505
+ begin
506
+ Timeout::timeout(@timeout) do
507
+ @request_type = "Bedrock/Pocket Edition"
508
+ retval = connect()
509
+ return retval unless retval == Retval::SUCCESS
510
+ # Perform handshake and acquire data
511
+ payload = "\x01".force_encoding('ASCII-8BIT') # unconnected ping
512
+ payload += [Time.now.to_i].pack('L!<').force_encoding('ASCII-8BIT') # current time as a long
513
+ payload += "\x00\xFF\xFF\x00\xFE\xFE\xFE\xFE\xFD\xFD\xFD\xFD\x12\x34\x56\x78".force_encoding('ASCII-8BIT') # magic number
514
+ payload += [2].pack('L!<').force_encoding('ASCII-8BIT') # client GUID as a long
515
+ @server.write(payload)
516
+ @server.flush
517
+ retval = parse_data("\x3B") # semicolon
518
+ end
519
+ rescue Timeout::Error
520
+ return Retval::TIMEOUT
521
+ rescue => exception
522
+ $stderr.puts exception
523
+ return Retval::UNKNOWN
524
+ end
525
+ return retval
526
+ end
527
+
528
+ # Returns the Minecraft server IP
529
+ attr_reader :address
530
+
531
+ # Returns the Minecraft server TCP port
532
+ attr_reader :port
533
+
534
+ # Returns a boolean describing whether the server is online or offline
535
+ attr_reader :online
536
+
537
+ # Returns the Minecraft version that the server is running
538
+ attr_reader :version
539
+
540
+ # Returns the game mode (Bedrock/Pocket Edition only)
541
+ attr_reader :mode
542
+
543
+ # Returns the full version of the MotD
544
+ #
545
+ # If you just want the MotD text, use stripped_motd
546
+ attr_reader :motd
547
+
548
+ # Returns just the plain text contained within the MotD
549
+ attr_reader :stripped_motd
550
+
551
+ # Returns the current player count
552
+ attr_reader :current_players
553
+
554
+ # Returns the maximum player count
555
+ attr_reader :max_players
556
+
557
+ # Returns the SLP (Server List Ping) protocol level
558
+ #
559
+ # This is arbitrary and varies by Minecraft version.
560
+ # However, multiple Minecraft versions can share the same
561
+ # protocol level
562
+ attr_reader :protocol
563
+
564
+ # Returns the complete JSON response data for queries to Minecraft
565
+ # servers with a version greater than or equal to 1.7
566
+ attr_reader :json_data
567
+
568
+ # Returns the ping time to the server in ms
569
+ attr_reader :latency
570
+
571
+ # Returns the SLP (Server List Ping) protocol version
572
+ attr_reader :request_type
365
573
  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.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lloyd Dilley
8
- - Stepan Melnikov
9
- autorequire:
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2021-03-03 00:00:00.000000000 Z
11
+ date: 2022-05-01 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,8 @@ 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.7.6.2
44
- signing_key:
41
+ rubygems_version: 3.3.7
42
+ signing_key:
45
43
  specification_version: 4
46
44
  summary: Minecraft server status checker
47
45
  test_files: []