class MineStat
Constants
- DEFAULT_PORT
- DEFAULT_TIMEOUT
- MAX_VARINT_SIZE
- NUM_FIELDS
- NUM_FIELDS_BETA
- VERSION
Attributes
Public Class Methods
# File lib/minestat.rb, line 46 def initialize(address, port = DEFAULT_PORT, timeout = DEFAULT_TIMEOUT, request_type = Request::NONE) @address = address # address of server @port = port # TCP port of server @online # online or offline? @version # server version @motd # message of the day @stripped_motd # message of the day without formatting @current_players # current number of players online @max_players # maximum player capacity @protocol # protocol level @json_data # JSON data for 1.7 queries @latency # ping time to server in milliseconds @timeout = timeout # TCP timeout @server # server socket @request_type # SLP protocol version case request_type when Request::BETA retval = beta_request() when Request::LEGACY retval = legacy_request() when Request::EXTENDED retval = extended_legacy_request() when Request::JSON retval = json_request() else # Attempt various SLP ping requests in a particular order. If the request # succeeds or the connection fails, there is no reason to continue with # subsequent requests. Attempts should continue in the event of a timeout # however since it may be due to an issue during the handshake. # Note: Newer server versions may still respond to older SLP requests. # For example, 1.13.2 responds to 1.4/1.5 queries, but not 1.6 queries. # SLP 1.4/1.5 retval = legacy_request() # SLP 1.8b/1.3 unless retval == Retval::SUCCESS || retval == Retval::CONNFAIL retval = beta_request() end # SLP 1.6 unless retval == Retval::CONNFAIL retval = extended_legacy_request() end # SLP 1.7 unless retval == Retval::CONNFAIL retval = json_request() end end @online = false unless retval == Retval::SUCCESS end
Public Instance Methods
1.8b/1.3 1.8 beta through 1.3 servers communicate as follows for a ping request:
-
Client sends xFE (server list ping)
-
Server responds with:
2a. \xFF (kick packet) 2b. data length 2c. 3 fields delimited by \u00A7 (section symbol)
The 3 fields, in order, are: message of the day, current players, and max players
# File lib/minestat.rb, line 185 def beta_request() retval = nil begin Timeout::timeout(@timeout) do retval = connect() return retval unless retval == Retval::SUCCESS # Perform handshake and acquire data @request_type = "SLP 1.8b/1.3 (beta)" @server.write("\xFE") retval = parse_data("\u00A7", true) # section symbol end rescue Timeout::Error return Retval::TIMEOUT rescue => exception $stderr.puts exception return Retval::UNKNOWN end return retval end
Connects to remote server
# File lib/minestat.rb, line 113 def connect() begin start_time = Time.now @server = TCPSocket.new(@address, @port) @latency = ((Time.now - start_time) * 1000).round rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH return Retval::CONNFAIL rescue => exception $stderr.puts exception return Retval::UNKNOWN end return Retval::SUCCESS end
1.6 1.6 servers communicate as follows for a ping request:
-
Client sends:
1a. \xFE (server list ping) 1b. \x01 (server list ping payload) 1c. \xFA (plugin message) 1d. \x00\x0B (11 which is the length of "MC|PingHost") 1e. "MC|PingHost" encoded as a UTF-16BE string 1f. length of remaining data as a short: remote address (encoded as UTF-16BE) + 7 1g. arbitrary 1.6 protocol version (\x4E for example for 78) 1h. length of remote address as a short 1i. remote address encoded as a UTF-16BE string 1j. remote port as an int
-
Server responds with:
2a. \xFF (kick packet) 2b. data length 2c. 6 fields delimited by \x00 (null)
The 6 fields, in order, are: the section symbol and 1, protocol version, server version, message of the day, current players, and max players The protocol version corresponds with the server version and can be the same for different server versions.
# File lib/minestat.rb, line 259 def extended_legacy_request() retval = nil begin Timeout::timeout(@timeout) do retval = connect() return retval unless retval == Retval::SUCCESS # Perform handshake and acquire data @request_type = "SLP 1.6 (extended legacy)" @server.write("\xFE\x01\xFA") @server.write("\x00\x0B") # 11 (length of "MC|PingHost") @server.write('MC|PingHost'.encode('UTF-16BE').force_encoding('ASCII-8BIT')) @server.write([7 + 2 * @address.length].pack('n')) @server.write("\x4E") # 78 (protocol version of 1.6.4) @server.write([@address.length].pack('n')) @server.write(@address.encode('UTF-16BE').force_encoding('ASCII-8BIT')) @server.write([@port].pack('N')) retval = parse_data("\x00") # null end rescue Timeout::Error return Retval::TIMEOUT rescue => exception $stderr.puts exception return Retval::UNKNOWN end return retval end
1.7 1.7 to current servers communicate as follows for a ping request:
-
Client sends:
1a. \x00 (handshake packet containing the fields specified below) 1b. \x00 (request)
The handshake packet contains the following fields respectively:
1. protocol version as a varint (\x00 suffices) 2. remote address as a string 3. remote port as an unsigned short 4. state as a varint (should be 1 for status)
-
Server responds with:
2a. \x00 (JSON response)
An example JSON string contains: {'players': {'max': 20, 'online': 0}, 'version': {'protocol': 404, 'name': '1.13.2'}, 'description': {'text': 'A Minecraft Server'}}
# File lib/minestat.rb, line 302 def json_request() retval = nil begin Timeout::timeout(@timeout) do retval = connect() return retval unless retval == Retval::SUCCESS # Perform handshake @request_type = "SLP 1.7 (JSON)" payload = "\x00\x00" payload += [@address.length].pack('c') << @address payload += [@port].pack('n') payload += "\x01" payload = [payload.length].pack('c') << payload @server.write(payload) @server.write("\x01\x00") @server.flush # Acquire data _total_len = unpack_varint return Retval::UNKNOWN if unpack_varint != 0 json_len = unpack_varint json_data = recv_json(json_len) @server.close # Parse data json_data = JSON.parse(json_data) @json_data = json_data @protocol = json_data['version']['protocol'].to_i @version = json_data['version']['name'] @motd = json_data['description'] strip_motd(true) @current_players = json_data['players']['online'].to_i @max_players = json_data['players']['max'].to_i if !@version.empty? && !@motd.empty? && !@current_players.nil? && !@max_players.nil? @online = true else retval = Retval::UNKNOWN end end rescue Timeout::Error return Retval::TIMEOUT rescue JSON::ParserError return Retval::UNKNOWN rescue => exception $stderr.puts exception return Retval::UNKNOWN end return retval end
1.4/1.5 1.4 and 1.5 servers communicate as follows for a ping request:
-
Client sends:
1a. \xFE (server list ping) 1b. \x01 (server list ping payload)
-
Server responds with:
2a. \xFF (kick packet) 2b. data length 2c. 6 fields delimited by \x00 (null)
The 6 fields, in order, are: the section symbol and 1, protocol version, server version, message of the day, current players, and max players The protocol version corresponds with the server version and can be the same for different server versions.
# File lib/minestat.rb, line 218 def legacy_request() retval = nil begin Timeout::timeout(@timeout) do retval = connect() return retval unless retval == Retval::SUCCESS # Perform handshake and acquire data @request_type = "SLP 1.4/1.5 (legacy)" @server.write("\xFE\x01") retval = parse_data("\x00") # null end rescue Timeout::Error return Retval::TIMEOUT rescue => exception $stderr.puts exception return Retval::UNKNOWN end return retval end
Populates object fields after connecting
# File lib/minestat.rb, line 128 def parse_data(delimiter, is_beta = false) data = nil begin if @server.read(1).unpack('C').first == 0xFF # kick packet (255) len = @server.read(2).unpack('n').first data = @server.read(len * 2).force_encoding('UTF-16BE').encode('UTF-8') @server.close else @server.close return Retval::UNKNOWN end rescue => exception $stderr.puts exception return Retval::UNKNOWN end if data == nil || data.empty? return Retval::UNKNOWN end server_info = data.split(delimiter) if is_beta if server_info != nil && server_info.length >= NUM_FIELDS_BETA @version = ">=1.8b/1.3" # since server does not return version, set it @motd = server_info[0] strip_motd @current_players = server_info[1].to_i @max_players = server_info[2].to_i @online = true else return Retval::UNKNOWN end else if server_info != nil && server_info.length >= NUM_FIELDS # server_info[0] contains the section symbol and 1 @protocol = server_info[1].to_i # contains the protocol version (51 for 1.9 or 78 for 1.6.4 for example) @version = server_info[2] @motd = server_info[3] strip_motd @current_players = server_info[4].to_i @max_players = server_info[5].to_i @online = true else return Retval::UNKNOWN end end return Retval::SUCCESS end
Reads JSON data from the socket
# File lib/minestat.rb, line 353 def recv_json(json_len) json_data = "" begin loop do remaining = json_len - json_data.length data = @server.recv(remaining) @server.flush json_data += data break if json_data.length >= json_len end rescue => exception $stderr.puts exception end return json_data end
Strips message of the day formatting characters
# File lib/minestat.rb, line 97 def strip_motd(is_json = false) unless is_json @stripped_motd = @motd.gsub(/§./, "") else @stripped_motd = @motd['text'] json_data = @motd['extra'] unless json_data.nil? || json_data.empty? json_data.each do |nested_hash| @stripped_motd += nested_hash['text'] end end @stripped_motd = @stripped_motd.gsub(/§./, "") end end
Returns value of varint type
# File lib/minestat.rb, line 370 def unpack_varint() vint = 0 i = 0 while i <= MAX_VARINT_SIZE data = @server.read(1) return 0 if data.nil? || data.empty? data = data.ord vint |= (data & 0x7F) << 7 * i break if (data & 0x80) != 128 i += 1 end return vint end