package org.royaldev.royalbot.commands.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.pircbotx.hooks.types.GenericMessageEvent;
import org.royaldev.royalbot.BotUtils;
import org.royaldev.royalbot.commands.CallInfo;
import org.royaldev.royalbot.commands.NoticeableCommand;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
public class MCPingCommand extends NoticeableCommand {
@Override
public void onCommand(GenericMessageEvent event, CallInfo callInfo, String[] args) {
if (args.length < 1) {
notice(event, "Not enough arguments.");
return;
}
final int port;
try {
port = (args.length > 1) ? Integer.valueOf(args[1]) : 25565;
} catch (NumberFormatException e) {
notice(event, BotUtils.formatException(e));
return;
}
final MinecraftPingReply mpr = new MinecraftPing().getPing(args[0], port);
if (mpr == null) {
notice(event, "Server appears to be down.");
return;
}
event.respond(mpr.getMotd() + " - " + mpr.getOnlinePlayers() + "/" + mpr.getMaxPlayers() + " players, " + mpr.getVersion());
}
@Override
public String getName() {
return "mcping";
}
@Override
public String getUsage() {
return "<command> [server] (port)";
}
@Override
public String getDescription() {
return "Pings a Minecraft server and returns its info.";
}
@Override
public String[] getAliases() {
return new String[0];
}
@Override
public CommandType getCommandType() {
return CommandType.BOTH;
}
@Override
public AuthLevel getAuthLevel() {
return AuthLevel.PUBLIC;
}
private class MinecraftPing {
private final ObjectMapper om = new ObjectMapper();
public MinecraftPingReply getPing(final String hostname, final int port) {
try {
return get17Ping(hostname, port);
} catch (IOException ex) {
try {
return get16Ping(hostname, port);
} catch (IOException e) {
return null;
}
}
}
public int readVarInt(DataInputStream in) throws IOException {
int i = 0;
int j = 0;
while (true) {
int k = in.readByte();
i |= (k & 0x7F) << j++ * 7;
if (j > 5) throw new RuntimeException("VarInt too big");
if ((k & 0x80) != 128) break;
}
return i;
}
public void writeVarInt(DataOutputStream out, int paramInt) throws IOException {
while (true) {
if ((paramInt & 0xFFFFFF80) == 0) {
out.writeByte(paramInt);
return;
}
out.writeByte(paramInt & 0x7F | 0x80);
paramInt >>>= 7;
}
}
public MinecraftPingReply get17Ping(final String hostname, final int port) throws IOException {
this.validate(hostname, "Hostname cannot be null.");
this.validate(port, "Port cannot be null.");
final Socket socket = new Socket();
socket.connect(new InetSocketAddress(hostname, port), 1750);
final DataInputStream in = new DataInputStream(socket.getInputStream());
final DataOutputStream out = new DataOutputStream(socket.getOutputStream());
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream prepare = new DataOutputStream(baos);
writeVarInt(prepare, 0x00);
writeVarInt(prepare, 4); // protocol version as of 1.7.2/4
writeVarInt(prepare, hostname.length());
prepare.writeBytes(hostname);
prepare.writeShort(port);
writeVarInt(prepare, 1);
prepare.flush();
writeVarInt(out, baos.size());
out.write(baos.toByteArray());
out.flush();
writeVarInt(out, 0x01);
writeVarInt(out, 0x00);
out.flush();
if (readVarInt(in) < 1) throw new IOException("Invalid packet size.");
if (readVarInt(in) != 0x00) throw new IOException("Invalid packet ID.");
final int length = readVarInt(in);
if (length < 1) throw new IOException("Invalid string length.");
byte[] bs = new byte[length];
in.readFully(bs);
final JsonNode jn = om.readTree(new String(bs));
return new MinecraftPingReply(hostname, port, jn.path("description").asText(), jn.path("version").path("protocol").asText(), jn.path("version").path("name").asText(), jn.path("players").path("online").asInt(), jn.path("players").path("max").asInt());
}
/**
* Fetches a {@link MinecraftPingReply} for the supplied hostname and port. Will revert to pre-12w42b ping message if required.
*
* @param hostname - the IP of the server to request ping from
* @param port - the port of the server to request ping from
* @return {@link MinecraftPingReply} - list of basic server information
* @throws IOException thrown when failed to receive packet or when incorrect packet is received
*/
public MinecraftPingReply get16Ping(final String hostname, final int port) throws IOException {
this.validate(hostname, "Hostname cannot be null.");
this.validate(port, "Port cannot be null.");
final Socket socket = new Socket();
socket.connect(new InetSocketAddress(hostname, port), 1750);
final DataInputStream in = new DataInputStream(socket.getInputStream());
final DataOutputStream out = new DataOutputStream(socket.getOutputStream());
out.write(0xFE);
out.write(0x01);
out.write(0xFA);
out.writeShort(11);
out.writeChars("MC|PingHost");
out.writeShort(7 + 2 * hostname.length());
out.writeByte(73); // Protocol version
out.writeShort(hostname.length());
out.writeChars(hostname);
out.writeInt(port);
out.flush();
if (in.read() != 255) throw new IOException("Bad message: An incorrect packet was received.");
final short bit = in.readShort();
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < bit; ++i) sb.append(in.readChar());
out.close();
final String[] bits = sb.toString().split("\0");
if (bits.length != 6) return this.getPing(sb.toString(), hostname, port);
return new MinecraftPingReply(hostname, port, bits[3], bits[1], bits[2], Integer.valueOf(bits[4]), Integer.valueOf(bits[5]));
}
/**
* Returns a {@link MinecraftPingReply} for the response supplied. <b>Only call from {@link MinecraftPing#getPing(String, int)}.</b>
* <p/>
* <p/>
* This method isn't intended for use outside of the {@link MinecraftPing} class.
*
* @param response - the pre-12w42b ping reply message
* @param hostname - the IP of the server ping was requested from
* @param port - the port of the server ping was requested from
* @return {@link MinecraftPingReply} - list of basic server information
* @throws IOException thrown when incorrect message supplied
*/
private MinecraftPingReply getPing(final String response, final String hostname, final int port) throws IOException {
this.validate(response, "Response cannot be null. Try calling MinecraftPing.getPing().");
this.validate(hostname, "Hostname cannot be null.");
this.validate(port, "Port cannot be null.");
final String[] bits = response.split("\u00a7");
if (bits.length != 3)
throw new IOException("Bad message: Failed to parse pre-12w42b ping message, check to see if it's correct?");
return new MinecraftPingReply(hostname, port, bits[0], Integer.valueOf(bits[2]), Integer.valueOf(bits[1]));
}
private void validate(final Object o, final String m) {
if (o == null) throw new RuntimeException(m);
}
}
public class MinecraftPingReply {
/**
* The IP of the server
*/
private final String ip;
/**
* The port of the server
*/
private final int port;
/**
* The MOTD of the server
*/
private final String motd;
/**
* The protocol version of the server
*/
private final String protocolVersion;
/**
* The game version of the server
*/
private final String version;
/**
* The max player count of the server
*/
private final int maxPlayers;
/**
* The current online player count of the server
*/
private final int onlinePlayers;
MinecraftPingReply(final String ip, final int port, final String motd, final int onlinePlayers, final int maxPlayers) {
this(ip, port, motd, "Pre-47", "Pre-12w42b", onlinePlayers, maxPlayers);
}
MinecraftPingReply(final String ip, final int port, final String motd, final String protocolVersion, final String version, final int onlinePlayers, final int maxPlayers) {
this.ip = ip;
this.port = port;
this.motd = motd.replaceAll("(?i)\\u00a7[0-9a-fk-or]", "");
this.protocolVersion = protocolVersion;
this.version = version;
this.maxPlayers = maxPlayers;
this.onlinePlayers = onlinePlayers;
}
/**
* Gets the server's IP
*
* @return the server's IP
*/
public String getIp() {
return this.ip;
}
/**
* Gets the server's maximum player count
*
* @return the server's maximum player count
*/
public int getMaxPlayers() {
return this.maxPlayers;
}
/**
* Gets the server's MOTD
*
* @return the server's MOTD
*/
public String getMotd() {
return this.motd;
}
/**
* Gets the server's current online player count
*
* @return the server's current online player count
*/
public int getOnlinePlayers() {
return this.onlinePlayers;
}
/**
* Gets the server's port
*
* @return the server's port
*/
public int getPort() {
return this.port;
}
/**
* Gets the server's protocol version
*
* @return the server's protocol version
*/
public String getProtocolVersion() {
return this.protocolVersion;
}
/**
* Gets the server's game version
*
* @return the server's game version
*/
public String getVersion() {
return this.version;
}
/**
* Returns a JSON representation of the data contained within this ping reply.
*/
@Override
public String toString() {
return String.format("{\"ip\":\"%s\",\"port\":%s,\"maxPlayers\":%s,\"onlinePlayers\":%s,\"motd\":\"%s\",\"protocolVersion\":\"%s\",\"serverVersion\":\"%s\"}", this.getIp(), this.getPort(), this.getMaxPlayers(), this.getOnlinePlayers(), this.getMotd(), this.getProtocolVersion(), this.getVersion());
}
}
}