Skip to content

Instantly share code, notes, and snippets.

@ribasco
Last active September 30, 2021 10:24
Show Gist options
  • Save ribasco/6a03ecdf3c63afcbda0785d3fa9d97fa to your computer and use it in GitHub Desktop.
Save ribasco/6a03ecdf3c63afcbda0785d3fa9d97fa to your computer and use it in GitHub Desktop.
Source Info Query in Java (New Update)
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;
class SourceInfoQuery {
private static final PrintStream out = System.out;
private static final PrintStream err = System.err;
private static final int RESPONSE_CHALLENGE = 0x41;
private static final int RESPONSE_INFO = 0x49;
public static void main(String[] args) throws Exception {
new Scratch().run();
}
private void run() throws Exception {
runBlockingSourceInfoQuery();
}
private void runBlockingSourceInfoQuery() throws Exception {
InetSocketAddress address = new InetSocketAddress("bacon.servebeer.com", 27017);
try (DatagramChannel channel = DatagramChannel.open().connect(address)) {
if (!channel.isConnected())
throw new Exception("Not connected");
Integer challenge = requestChallenge(channel);
final int requestInterval = 1000;
boolean done = false;
while (!done) {
try {
ByteBuffer response = sendInfoRequest(channel, challenge);
//parse response
int header = response.getInt();
byte identifier = response.get();
String name = readString(response);
String map = readString(response);
String folder = readString(response);
String game = readString(response);
int appId = response.getShort();
int playerCount = Byte.toUnsignedInt(response.get());
int maxPlayerCount = Byte.toUnsignedInt(response.get());
int botCount = Byte.toUnsignedInt(response.get());
String serverType = new String(new byte[] {response.get()}, StandardCharsets.US_ASCII);
String environmentType = new String(new byte[] {response.get()}, StandardCharsets.US_ASCII);
String visibility = response.get() == 0 ? "public" : "private";
int vac = response.get();
String version = readString(response);
int flags = Byte.toUnsignedInt(response.get());
int port = -1;
if ((flags & 0x80) == 0x80) {
port = response.getShort();
}
long steamId = -1;
if ((flags & 0x10) == 0x10) {
steamId = response.getLong();
}
int specPort = -1;
String specName = null;
if ((flags & 0x40) == 0x40) {
specPort = response.getShort();
specName = readString(response);
}
String tags = null;
if ((flags & 0x20) == 0x20) {
tags = readString(response);
}
long gameId = -1;
if ((flags & 0x01) == 0x01) {
gameId = response.getLong();
}
printField("Header", header);
printField("Identifier", identifier);
printField("Name", name);
printField("Map", map);
printField("Folder", folder);
printField("Game", game);
printField("App ID", appId);
printField("Player Count", playerCount);
printField("Max Players", maxPlayerCount);
printField("Bots", botCount);
printField("Server Type", serverType);
printField("Environment", environmentType);
printField("Visibility", visibility);
printField("VAC", vac);
printField("Version", version);
printField("Port", port);
printField("SteamID", steamId);
printField("Spectator Port", specPort);
printField("Spectator Name", specName);
printField("Tags", tags);
printField("Game ID", gameId);
//out.printf("\t%d) Got response: %d bytes (Header: %d, Indentifier: %s, Name: %s, Map: %s, Challenge: %d)\n", ++requestCtr, response.limit(), header, printHex(new byte[] {identifier}), name, map, challenge);
} catch (SourceChallengeRequiredException e) {
challenge = e.getNewChallenge();
err.printf("\t(new challenge) Server updated challenge number to %d\n", challenge);
} finally {
Thread.sleep(requestInterval);
}
}
}
}
//NOTE:
//To use new source query, set environment variable STEAM_GAMESERVER_A2S_INFO_STRICT_LEGACY_PROTOCOL to 0
//to use legacy implementation, set STEAM_GAMESERVER_A2S_INFO_STRICT_LEGACY_PROTOCOL to 1
//The challenge number is updated every 16 seconds as per my test
//If we send a request packet of size >= 1004 and <= 1400 bytes, with or without a challenge number, the challenge number is discarded and the server will reply with a 0x49
private ByteBuffer sendInfoRequest(DatagramChannel channel, Integer challenge) throws IOException {
if (!channel.isOpen() || !channel.isConnected())
throw new IOException("Not open or connected");
ByteBuffer request = createInfoRequest(challenge);
out.printf("(request) Sending INFO request (challenge: %d, size: %d): %s\n", challenge, request.array().length, toHexString(request.array()));
int bytesSent = channel.write(request);
//out.printf("(request) Sent %d bytes\n", bytesSent);
if (bytesSent == 0)
throw new IOException("No bytes were sent");
ByteBuffer response = ByteBuffer.allocate(1400).order(ByteOrder.LITTLE_ENDIAN);
int bytesReceived = channel.read(response);
if (bytesReceived == 0)
throw new IOException("Received nothing from server");
out.printf("(response) Received %d bytes (data: %s)\n", bytesReceived, toHexString(response.array()));
response.flip();
//did the server respond with a challenge?
byte identifier = response.array()[4];
if (identifier != 0x49) {
if (identifier == 0x41) {
int newChallengeNumber = extractChallenge(response);
throw new SourceChallengeRequiredException(String.format("Server responded with a new challenge number: %d", newChallengeNumber), newChallengeNumber);
}
throw new IllegalStateException("Received an invalid/unrecognized response from the server. Possibly packet is malformed?");
}
return response;
}
private ByteBuffer createChallengeRequest() {
return createInfoRequest();
}
private int requestChallenge(DatagramChannel channel) throws Exception {
if (!channel.isOpen() || !channel.isConnected())
throw new Exception("Not open or connected");
ByteBuffer request = createChallengeRequest();
int bytesSent = channel.write(request);
out.printf("(challenge request) Sent %d bytes%n", bytesSent);
ByteBuffer response = ByteBuffer.allocate(1400).order(ByteOrder.LITTLE_ENDIAN);
if (!channel.isConnected())
throw new Exception("Channel is not connected");
int bytesReceived = channel.read(response);
//out.printf("(challenge response) Received %d bytes from server\n", bytesReceived);
//read server challenge response
int newChallenge = extractChallenge(response);
response.clear();
response.limit(bytesReceived);
out.printf("(challenge response) %s (Value: %d, Size: %d)\n", toHexString(toByteArray(newChallenge)), newChallenge, bytesReceived);
return newChallenge;
}
private int extractChallenge(ByteBuffer buffer) {
//read server challenge response
buffer.clear();
int header = buffer.getInt();
int identifer = buffer.get();
int challenge = buffer.getInt();
//server responded with a challenge
if (identifer == RESPONSE_CHALLENGE) {
return challenge;
} else if (identifer == RESPONSE_INFO) {
return -1; //server responded with an info type response (0x49)
} else {
throw new IllegalStateException(String.format("Invalid response from the server (header:%s, identifier: %s)", toHexString(toByteArray(header)), toHexString(toByteArray(identifer))));
}
}
private static void printField(String name, Object value) {
out.printf(" %-15s: ", name);
if (value instanceof Number) {
out.printf("%-10d", value);
} else if (value instanceof String || value instanceof Character) {
out.printf("%-10s", value);
}
out.println();
}
private static String readString(ByteBuffer buffer) {
StringBuilder sb = new StringBuilder();
while (buffer.hasRemaining()) {
byte b = buffer.get();
if (b == 0)
break;
sb.append((char) b);
}
return new String(sb.toString().getBytes(), StandardCharsets.UTF_8);
}
private static byte[] toByteArray(int value) {
return new byte[] {
(byte) (value >> 24),
(byte) (value >> 16),
(byte) (value >> 8),
(byte) value};
}
private static String toHexString(byte[] data) {
StringBuilder res = new StringBuilder();
for (int i = 0; i < data.length; i++) {
res.append("0x");
res.append(String.format("%02x", data[i]).toUpperCase());
if (i < data.length - 1)
res.append(" ");
}
return res.toString();
}
private ByteBuffer createInfoRequest() {
return createInfoRequest(null);
}
private ByteBuffer createInfoRequest(Integer challenge) {
ByteBuffer buf;
if (challenge == null) {
buf = ByteBuffer.allocate(25).order(ByteOrder.LITTLE_ENDIAN); //1004, 29
} else {
buf = ByteBuffer.allocate(29).order(ByteOrder.LITTLE_ENDIAN);
}
//ByteBuffer.allocate(29).order(ByteOrder.LITTLE_ENDIAN); //1004, 29
buf.putInt(0xFFFFFFFF);
buf.put((byte) 0x54);
buf.put("Source Engine Query\0".getBytes());
if (challenge != null)
buf.putInt(challenge);
buf.clear();
return buf;
}
private static class SourceChallengeRequiredException extends IOException {
private final int newChallenge;
SourceChallengeRequiredException(String message, int newChallenge) {
super(message);
this.newChallenge = newChallenge;
}
int getNewChallenge() {
return newChallenge;
}
}
}import java.io.IOException;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;
class Scratch {
private static final PrintStream out = System.out;
private static final PrintStream err = System.err;
private static final int RESPONSE_CHALLENGE = 0x41;
private static final int RESPONSE_INFO = 0x49;
public static void main(String[] args) throws Exception {
new Scratch().run();
}
private void run() throws Exception {
runBlockingSourceInfoQuery();
}
private void runBlockingSourceInfoQuery() throws Exception {
InetSocketAddress address = new InetSocketAddress("bacon.servebeer.com", 27017);//new InetSocketAddress("116.251.214.166", 13371);//new InetSocketAddress("139.99.125.114", 19001);//new InetSocketAddress("139.99.125.114", 19001);//new InetSocketAddress("112.207.114.235", 27015);
try (DatagramChannel channel = DatagramChannel.open().connect(address)) {
if (!channel.isConnected())
throw new Exception("Not connected");
Integer challenge = requestChallenge(channel);
final int requestInterval = 1000;
boolean done = false;
while (!done) {
try {
ByteBuffer response = sendInfoRequest(channel, challenge);
//parse response
int header = response.getInt();
byte identifier = response.get();
String name = readString(response);
String map = readString(response);
String folder = readString(response);
String game = readString(response);
int appId = response.getShort();
int playerCount = Byte.toUnsignedInt(response.get());
int maxPlayerCount = Byte.toUnsignedInt(response.get());
int botCount = Byte.toUnsignedInt(response.get());
String serverType = new String(new byte[] {response.get()}, StandardCharsets.US_ASCII);
String environmentType = new String(new byte[] {response.get()}, StandardCharsets.US_ASCII);
String visibility = response.get() == 0 ? "public" : "private";
int vac = response.get();
String version = readString(response);
int flags = Byte.toUnsignedInt(response.get());
int port = -1;
if ((flags & 0x80) == 0x80) {
port = response.getShort();
}
long steamId = -1;
if ((flags & 0x10) == 0x10) {
steamId = response.getLong();
}
int specPort = -1;
String specName = null;
if ((flags & 0x40) == 0x40) {
specPort = response.getShort();
specName = readString(response);
}
String tags = null;
if ((flags & 0x20) == 0x20) {
tags = readString(response);
}
long gameId = -1;
if ((flags & 0x01) == 0x01) {
gameId = response.getLong();
}
printField("Header", header);
printField("Identifier", identifier);
printField("Name", name);
printField("Map", map);
printField("Folder", folder);
printField("Game", game);
printField("App ID", appId);
printField("Player Count", playerCount);
printField("Max Players", maxPlayerCount);
printField("Bots", botCount);
printField("Server Type", serverType);
printField("Environment", environmentType);
printField("Visibility", visibility);
printField("VAC", vac);
printField("Version", version);
printField("Port", port);
printField("SteamID", steamId);
printField("Spectator Port", specPort);
printField("Spectator Name", specName);
printField("Tags", tags);
printField("Game ID", gameId);
//out.printf("\t%d) Got response: %d bytes (Header: %d, Indentifier: %s, Name: %s, Map: %s, Challenge: %d)\n", ++requestCtr, response.limit(), header, printHex(new byte[] {identifier}), name, map, challenge);
} catch (SourceChallengeRequiredException e) {
challenge = e.getNewChallenge();
err.printf("\t(new challenge) Server updated challenge number to %d\n", challenge);
} finally {
Thread.sleep(requestInterval);
}
}
}
}
//NOTE:
//To use new source query, set environment variable STEAM_GAMESERVER_A2S_INFO_STRICT_LEGACY_PROTOCOL to 0
//to use legacy implementation, set STEAM_GAMESERVER_A2S_INFO_STRICT_LEGACY_PROTOCOL to 1
//The challenge number is updated every 16 seconds as per my test
//If we send a request packet of size >= 1004 and <= 1400 bytes, with or without a challenge number, the challenge number is discarded and the server will reply with a 0x49
private ByteBuffer sendInfoRequest(DatagramChannel channel, Integer challenge) throws IOException {
if (!channel.isOpen() || !channel.isConnected())
throw new IOException("Not open or connected");
ByteBuffer request = createInfoRequest(challenge);
out.printf("(request) Sending INFO request (challenge: %d, size: %d): %s\n", challenge, request.array().length, toHexString(request.array()));
int bytesSent = channel.write(request);
//out.printf("(request) Sent %d bytes\n", bytesSent);
if (bytesSent == 0)
throw new IOException("No bytes were sent");
ByteBuffer response = ByteBuffer.allocate(1400).order(ByteOrder.LITTLE_ENDIAN);
int bytesReceived = channel.read(response);
if (bytesReceived == 0)
throw new IOException("Received nothing from server");
out.printf("(response) Received %d bytes (data: %s)\n", bytesReceived, toHexString(response.array()));
response.flip();
//did the server respond with a challenge?
byte identifier = response.array()[4];
if (identifier != 0x49) {
if (identifier == 0x41) {
int newChallengeNumber = extractChallenge(response);
throw new SourceChallengeRequiredException(String.format("Server responded with a new challenge number: %d", newChallengeNumber), newChallengeNumber);
}
throw new IllegalStateException("Received an invalid/unrecognized response from the server. Possibly packet is malformed?");
}
return response;
}
private ByteBuffer createChallengeRequest() {
return createInfoRequest();
}
private int requestChallenge(DatagramChannel channel) throws Exception {
if (!channel.isOpen() || !channel.isConnected())
throw new Exception("Not open or connected");
ByteBuffer request = createChallengeRequest();
int bytesSent = channel.write(request);
out.printf("(challenge request) Sent %d bytes%n", bytesSent);
ByteBuffer response = ByteBuffer.allocate(1400).order(ByteOrder.LITTLE_ENDIAN);
if (!channel.isConnected())
throw new Exception("Channel is not connected");
int bytesReceived = channel.read(response);
//out.printf("(challenge response) Received %d bytes from server\n", bytesReceived);
//read server challenge response
int newChallenge = extractChallenge(response);
response.clear();
response.limit(bytesReceived);
out.printf("(challenge response) %s (Value: %d, Size: %d)\n", toHexString(toByteArray(newChallenge)), newChallenge, bytesReceived);
return newChallenge;
}
private int extractChallenge(ByteBuffer buffer) {
//read server challenge response
buffer.clear();
int header = buffer.getInt();
int identifer = buffer.get();
int challenge = buffer.getInt();
//server responded with a challenge
if (identifer == RESPONSE_CHALLENGE) {
return challenge;
} else if (identifer == RESPONSE_INFO) {
return -1; //server responded with an info type response (0x49)
} else {
throw new IllegalStateException(String.format("Invalid response from the server (header:%s, identifier: %s)", toHexString(toByteArray(header)), toHexString(toByteArray(identifer))));
}
}
private static void printField(String name, Object value) {
out.printf(" %-15s: ", name);
if (value instanceof Number) {
out.printf("%-10d", value);
} else if (value instanceof String || value instanceof Character) {
out.printf("%-10s", value);
}
out.println();
}
private static String readString(ByteBuffer buffer) {
StringBuilder sb = new StringBuilder();
while (buffer.hasRemaining()) {
byte b = buffer.get();
if (b == 0)
break;
sb.append((char) b);
}
return new String(sb.toString().getBytes(), StandardCharsets.UTF_8);
}
private static byte[] toByteArray(int value) {
return new byte[] {
(byte) (value >> 24),
(byte) (value >> 16),
(byte) (value >> 8),
(byte) value};
}
private static String toHexString(byte[] data) {
StringBuilder res = new StringBuilder();
for (int i = 0; i < data.length; i++) {
res.append("0x");
res.append(String.format("%02x", data[i]).toUpperCase());
if (i < data.length - 1)
res.append(" ");
}
return res.toString();
}
private ByteBuffer createInfoRequest() {
return createInfoRequest(null);
}
private ByteBuffer createInfoRequest(Integer challenge) {
ByteBuffer buf;
if (challenge == null) {
buf = ByteBuffer.allocate(25).order(ByteOrder.LITTLE_ENDIAN); //1004, 29
} else {
buf = ByteBuffer.allocate(29).order(ByteOrder.LITTLE_ENDIAN);
}
//ByteBuffer.allocate(29).order(ByteOrder.LITTLE_ENDIAN); //1004, 29
buf.putInt(0xFFFFFFFF);
buf.put((byte) 0x54);
buf.put("Source Engine Query\0".getBytes());
if (challenge != null)
buf.putInt(challenge);
buf.clear();
return buf;
}
private static class SourceChallengeRequiredException extends IOException {
private final int newChallenge;
SourceChallengeRequiredException(String message, int newChallenge) {
super(message);
this.newChallenge = newChallenge;
}
int getNewChallenge() {
return newChallenge;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment