2013-07-15 23:01:01 +12:00
import socket
import struct
2013-11-27 03:24:11 -08:00
import json
2014-02-23 13:30:24 +13:00
import traceback
2013-11-27 03:24:11 -08:00
2014-02-14 16:36:57 +13:00
from util import hook
2013-07-15 23:01:01 +12:00
try :
2013-09-04 18:32:17 +08:00
import DNS
2014-02-22 22:04:39 +13:00
has_dns = True
2013-07-15 23:01:01 +12:00
except ImportError :
2014-02-22 22:04:39 +13:00
has_dns = False
2013-07-15 23:01:01 +12:00
2013-11-29 18:16:17 +13:00
2014-02-22 22:04:39 +13:00
mc_colors = [ ( u ' \xa7 f ' , u ' \x03 00 ' ) , ( u ' \xa7 0 ' , u ' \x03 01 ' ) , ( u ' \xa7 1 ' , u ' \x03 02 ' ) , ( u ' \xa7 2 ' , u ' \x03 03 ' ) ,
( u ' \xa7 c ' , u ' \x03 04 ' ) , ( u ' \xa7 4 ' , u ' \x03 05 ' ) , ( u ' \xa7 5 ' , u ' \x03 06 ' ) , ( u ' \xa7 6 ' , u ' \x03 07 ' ) ,
( u ' \xa7 e ' , u ' \x03 08 ' ) , ( u ' \xa7 a ' , u ' \x03 09 ' ) , ( u ' \xa7 3 ' , u ' \x03 10 ' ) , ( u ' \xa7 b ' , u ' \x03 11 ' ) ,
( u ' \xa7 1 ' , u ' \x03 12 ' ) , ( u ' \xa7 d ' , u ' \x03 13 ' ) , ( u ' \xa7 8 ' , u ' \x03 14 ' ) , ( u ' \xa7 7 ' , u ' \x03 15 ' ) ,
( u ' \xa7 l ' , u ' \x02 ' ) , ( u ' \xa7 9 ' , u ' \x03 10 ' ) , ( u ' \xa7 o ' , u ' \t ' ) , ( u ' \xa7 m ' , u ' \x13 ' ) ,
( u ' \xa7 r ' , u ' \x0f ' ) , ( u ' \xa7 n ' , u ' \x15 ' ) ]
2013-11-27 03:24:11 -08:00
2013-09-04 18:32:17 +08:00
2014-02-22 22:04:39 +13:00
## EXCEPTIONS
class PingError ( Exception ) :
def __init__ ( self , text ) :
self . text = text
def __str__ ( self ) :
return self . text
class ParseError ( Exception ) :
def __init__ ( self , text ) :
self . text = text
def __str__ ( self ) :
return self . text
## MISC
2013-07-15 23:01:01 +12:00
2013-11-27 03:24:11 -08:00
def unpack_varint ( s ) :
d = 0
i = 0
while True :
b = ord ( s . recv ( 1 ) )
d | = ( b & 0x7F ) << 7 * i
i + = 1
if not b & 0x80 :
return d
2013-07-15 23:01:01 +12:00
2014-02-22 22:04:39 +13:00
pack_data = lambda d : struct . pack ( ' >b ' , len ( d ) ) + d
pack_port = lambda i : struct . pack ( ' >H ' , i )
2013-07-15 23:01:01 +12:00
2014-02-22 22:04:39 +13:00
## DATA FUNCTIONS
2013-07-15 23:01:01 +12:00
2013-11-27 03:24:11 -08:00
2013-11-29 11:45:18 +13:00
def mcping_modern ( host , port ) :
2014-02-22 22:04:39 +13:00
""" pings a server using the modern (1.7+) protocol and returns data """
2014-02-23 19:35:51 +13:00
try :
2014-03-01 13:12:48 +13:00
# connect to the server
s = socket . socket ( socket . AF_INET , socket . SOCK_STREAM )
try :
s . connect ( ( host , port ) )
except socket . gaierror :
raise PingError ( " Invalid hostname " )
except socket . timeout :
raise PingError ( " Request timed out " )
# send handshake + status request
s . send ( pack_data ( " \x00 \x00 " + pack_data ( host . encode ( ' utf8 ' ) ) + pack_port ( port ) + " \x01 " ) )
s . send ( pack_data ( " \x00 " ) )
# read response
unpack_varint ( s ) # Packet length
unpack_varint ( s ) # Packet ID
l = unpack_varint ( s ) # String length
if not l > 1 :
raise PingError ( " Invalid response " )
d = " "
while len ( d ) < l :
d + = s . recv ( 1024 )
# Close our socket
s . close ( )
except socket . error :
raise PingError ( " Socket Error " )
2013-11-27 03:24:11 -08:00
# Load json and return
2013-11-29 11:45:18 +13:00
data = json . loads ( d . decode ( ' utf8 ' ) )
2013-11-27 03:24:11 -08:00
try :
version = data [ " version " ] [ " name " ]
2014-02-23 13:30:24 +13:00
try :
2014-02-16 17:26:53 +13:00
desc = u " " . join ( data [ " description " ] [ " text " ] . split ( ) )
2014-02-23 13:30:24 +13:00
except TypeError :
2014-02-16 17:26:53 +13:00
desc = u " " . join ( data [ " description " ] . split ( ) )
2014-02-14 17:03:08 +13:00
max_players = data [ " players " ] [ " max " ]
2013-11-27 03:24:11 -08:00
online = data [ " players " ] [ " online " ]
except Exception as e :
2014-02-25 13:36:52 +13:00
# TODO: except Exception is bad
2014-02-23 13:30:24 +13:00
traceback . print_exc ( e )
raise PingError ( " Unknown Error: {} " . format ( e ) )
2014-02-22 22:04:39 +13:00
output = {
" motd " : format_colors ( desc ) ,
" motd_raw " : desc ,
" version " : version ,
" players " : online ,
" players_max " : max_players
}
return output
2013-11-27 03:24:11 -08:00
2013-11-29 11:45:18 +13:00
def mcping_legacy ( host , port ) :
2014-02-22 22:04:39 +13:00
""" pings a server using the legacy (1.6 and older) protocol and returns data """
2013-11-27 03:24:11 -08:00
sock = socket . socket ( socket . AF_INET , socket . SOCK_STREAM )
2014-02-23 19:35:51 +13:00
2014-02-23 13:30:24 +13:00
try :
sock . connect ( ( host , port ) )
sock . send ( ' \xfe \x01 ' )
response = sock . recv ( 1 )
2014-02-23 19:35:51 +13:00
except socket . gaierror :
raise PingError ( " Invalid hostname " )
2014-02-23 13:30:24 +13:00
except socket . timeout :
raise PingError ( " Request timed out " )
2014-02-23 19:35:51 +13:00
2013-11-27 03:24:11 -08:00
if response [ 0 ] != ' \xff ' :
2014-02-23 13:30:24 +13:00
raise PingError ( " Invalid response " )
2013-11-27 03:24:11 -08:00
length = struct . unpack ( ' !h ' , sock . recv ( 2 ) ) [ 0 ]
values = sock . recv ( length * 2 ) . decode ( ' utf-16be ' )
data = values . split ( u ' \x00 ' ) # try to decode data using new format
if len ( data ) == 1 :
# failed to decode data, server is using old format
data = values . split ( u ' \xa7 ' )
2014-02-22 22:04:39 +13:00
output = {
2014-02-24 02:15:58 +13:00
" motd " : format_colors ( " " . join ( data [ 0 ] . split ( ) ) ) ,
2014-02-22 22:04:39 +13:00
" motd_raw " : data [ 0 ] ,
" version " : None ,
" players " : data [ 1 ] ,
" players_max " : data [ 2 ]
}
2013-11-27 03:24:11 -08:00
else :
# decoded data, server is using new format
2014-02-22 22:04:39 +13:00
output = {
2014-02-24 02:15:58 +13:00
" motd " : format_colors ( " " . join ( data [ 3 ] . split ( ) ) ) ,
2014-02-22 22:04:39 +13:00
" motd_raw " : data [ 3 ] ,
" version " : data [ 2 ] ,
" players " : data [ 4 ] ,
" players_max " : data [ 5 ]
}
2013-11-27 03:24:11 -08:00
sock . close ( )
2014-02-22 22:04:39 +13:00
return output
2013-07-15 23:01:01 +12:00
2014-02-22 22:04:39 +13:00
## FORMATTING/PARSING FUNCTIONS
def check_srv ( domain ) :
2013-11-29 11:45:18 +13:00
""" takes a domain and finds minecraft SRV records """
2014-02-13 20:07:01 +13:00
DNS . DiscoverNameServers ( )
2013-07-15 23:01:01 +12:00
srv_req = DNS . Request ( qtype = ' srv ' )
srv_result = srv_req . req ( ' _minecraft._tcp. {} ' . format ( domain ) )
for getsrv in srv_result . answers :
if getsrv [ ' typename ' ] == ' SRV ' :
2013-09-04 18:32:17 +08:00
data = [ getsrv [ ' data ' ] [ 2 ] , getsrv [ ' data ' ] [ 3 ] ]
2013-07-15 23:01:01 +12:00
return data
2013-11-29 11:45:18 +13:00
def parse_input ( inp ) :
""" takes the input from the mcping command and returns the host and port """
2013-07-15 23:01:01 +12:00
inp = inp . strip ( ) . split ( " " ) [ 0 ]
if " : " in inp :
2014-02-22 22:04:39 +13:00
# the port is defined in the input string
2013-07-15 23:01:01 +12:00
host , port = inp . split ( " : " , 1 )
try :
port = int ( port )
2014-02-22 22:04:39 +13:00
if port > 65535 or port < 0 :
raise ParseError ( " The port ' {} ' is invalid. " . format ( port ) )
except ValueError :
raise ParseError ( " The port ' {} ' is invalid. " . format ( port ) )
2013-11-27 03:24:11 -08:00
return host , port
2014-02-22 22:04:39 +13:00
if has_dns :
# the port is not in the input string, but we have PyDNS so look for a SRV record
srv_data = check_srv ( inp )
2013-11-27 03:24:11 -08:00
if srv_data :
return str ( srv_data [ 1 ] ) , int ( srv_data [ 0 ] )
2014-02-22 22:04:39 +13:00
# return default port
2013-11-27 03:24:11 -08:00
return inp , 25565
2013-07-15 23:01:01 +12:00
2013-11-27 03:24:11 -08:00
2014-02-22 22:04:39 +13:00
def format_colors ( motd ) :
for original , replacement in mc_colors :
motd = motd . replace ( original , replacement )
motd = motd . replace ( u " \xa7 k " , " " )
return motd
2013-11-27 03:24:11 -08:00
2014-02-22 22:04:39 +13:00
def format_output ( data ) :
if data [ " version " ] :
return u " {motd} \x0f - {version} \x0f - {players} / {players_max} " \
u " players. " . format ( * * data ) . replace ( " \n " , u " \x0f - " )
else :
return u " {motd} \x0f - {players} / {players_max} " \
u " players. " . format ( * * data ) . replace ( " \n " , u " \x0f - " )
2013-11-27 03:24:11 -08:00
@hook.command
@hook.command ( " mcp " )
def mcping ( inp ) :
""" mcping <server>[:port] - Ping a Minecraft server to check status. """
2014-02-22 22:04:39 +13:00
try :
host , port = parse_input ( inp )
except ParseError as e :
2014-02-23 19:35:51 +13:00
return " Could not parse input ( {} ) " . format ( e )
2014-02-13 20:07:01 +13:00
2013-11-27 03:24:11 -08:00
try :
2014-02-22 22:04:39 +13:00
data = mcping_modern ( host , port )
except PingError :
2013-11-27 03:24:11 -08:00
try :
2014-02-22 22:04:39 +13:00
data = mcping_legacy ( host , port )
2014-02-23 13:30:24 +13:00
except PingError as e :
2014-02-23 19:35:51 +13:00
return " Could not ping server, is it offline? ( {} ) " . format ( e )
2014-02-23 13:30:24 +13:00
return format_output ( data )