Last active
September 30, 2021 10:24
-
-
Save ribasco/6a03ecdf3c63afcbda0785d3fa9d97fa to your computer and use it in GitHub Desktop.
Source Info Query in Java (New Update)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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