Query number of players on a minecraft server

Without the minecraft client, there is a scripted (php, python, whatever) way to ask basic information (what you see in the multiplayer menu) to a minecraft server.

Does anyone knows the few magical bytes to send on the port 25565 ?


Solution 1:

Before the 1.7 version, a custom TCP protocol was used, and thus some escaped hexadecimal through a netcat / telnet did worked.

Today, they use JSON objects, and a more complex protocol, as implemented on the next link. On the wiki page little python script was linked : https://gist.github.com/barneygale/1209061.

I made this small implementation (freely inspired from last link) which prints the JSON object answered by the Minecraft server (localhost:25565 by default)

#!/usr/bin/env python3
import sys,json,struct,socket

def popint(s):
  acc = 0
  b = ord(s.recv(1))
  while b & 0x80:
    acc = (acc<<7)+(b&0x7f)
    b = ord(s.recv(1))
  return (acc<<7)+(b&0x7f)

def pack_varint(d):
  return bytes([(0x40*(i!=d.bit_length()//7))+((d>>(7*(i)))%128) for i in range(1+d.bit_length()//7)])

def pack_data(d):
  return pack_varint(len(d)) + d

def get_info(host,port):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((host, port))
  s.send(pack_data(bytes(2)+pack_data(bytes(host,'utf8'))+struct.pack('>H',port)+bytes([1]))+bytes([1,0]))
  popint(s)   # Packet length
  popint(s)   # Packet ID
  l,d = popint(s),bytes()
  while len(d) < l: d += s.recv(1024)
  s.close()
  return json.loads(d.decode('utf8'))

if __name__ == '__main__':
  host = sys.argv[1] if len(sys.argv) > 1 else 'localhost'
  port = int(sys.argv[2]) if len(sys.argv) > 2 else 25565
  print(get_info(host,port))

Downloadable here https://gist.github.com/qolund/6d10c02f331ca8ee047f

Edit : minimal version, use it with python3 script.py host port

import json,sys,socket as S
h,p=sys.argv[1:]
p=int(p)
u,K,L='utf8',bytes,len
s=S.socket(2,1);s.connect((h,p))
def z():
 a,b=0,s.recv(1)[0]
 while b&128:a,b=(a<<7)+b&127,s.recv(1)[0]
 return b&127+(a<<7)
def V(d,b):return K([(64*(i!=b//7))+((d>>(7*(i)))%128)for i in range(1+b//7)])
def D(d):return V(L(d),L(d).bit_length())+d
s.send(D(K(2)+D(K(h,u))+K([p>>8,p%256,1]))+K([1,0]))
z();z();l,d=z(),K()
while L(d)<l:d+=s.recv(1024)
s.close()
print(json.loads(str(d,u)))

Solution 2:

(Not an answer as it expands on Nope's, but too long for a comment, and I need code formatting)

Nope's code breaks for me (MC 1.10) as popint doesn't seem to decode multi-byte integers correctly; bytes are received in little endian order (lowest byte first). If a server has a large icon, the length doesn't get decoded correctly, which, in some cases, works anyway (as the code always reads 1024 bytes even when l is smaller), in others, you get an error from the JSON decoder about an unterminated string.

Replacing the popint function with this fixes the issue:

def popint(s):
  acc = 0
  shift=0
  b = ord(s.recv(1))
  while b & 0x80:
    acc = acc | ((b&0x7f)<<shift)
    shift = shift + 7
    b = ord(s.recv(1))
  return (acc)|(b<<shift)