Пинг ванильным методом и причины отказа по токину
- StatusSession: ответ на ванильный статус на том же порту. Первый байт потока различает наш протокол и гостей: R - наш кадр, иначе старый пинг (FE) или новый handshake + request + ping. Логин в старом стиле получает вежливый отказ словами. - Reader: первый байт, уже прочитанный вручную, теперь правильно исключается из головы - раньше счётчик дважды пропускал его. - TokenAuth: точные причины отказа (не подписано, другой сервер, не тот вид, истёк, не тот игрок) и Verdict для каждой сессии. - ClientSession: первое чтение смотрит на байт и отдаёт гостей статусу; остальное идёт через прежнее расщепление. - StatusTest: 5 проверок (json, modern, pong, legacy, отказ). Легаси-пинг читался как знаковый байт; исправлено на беззнаковый. - StatusRouteTest: 3 проверки через настоящий сервер и настоящие порты. - watch-game.ps1: удаляет устаревший dev-token перед стартом сервера.
This commit is contained in:
@@ -55,6 +55,8 @@ $suites = @(
|
|||||||
'net.rustnotmc.server.hitbox.HitboxTest',
|
'net.rustnotmc.server.hitbox.HitboxTest',
|
||||||
'net.rustnotmc.server.player.PlayerDataTest',
|
'net.rustnotmc.server.player.PlayerDataTest',
|
||||||
'net.rustnotmc.server.combat.DamageTest',
|
'net.rustnotmc.server.combat.DamageTest',
|
||||||
|
'net.rustnotmc.server.status.StatusTest',
|
||||||
|
'net.rustnotmc.server.status.StatusRouteTest',
|
||||||
'net.rustnotmc.server.PlayTest'
|
'net.rustnotmc.server.PlayTest'
|
||||||
)
|
)
|
||||||
foreach ($suite in $suites) {
|
foreach ($suite in $suites) {
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ public final class ClientSession implements Runnable {
|
|||||||
private volatile int playerId = -1;
|
private volatile int playerId = -1;
|
||||||
private volatile int entityId = -1;
|
private volatile int entityId = -1;
|
||||||
private volatile String lastError;
|
private volatile String lastError;
|
||||||
|
private boolean firstLook = true;
|
||||||
|
|
||||||
private volatile Player player;
|
private volatile Player player;
|
||||||
|
|
||||||
private byte[] nonce;
|
private byte[] nonce;
|
||||||
@@ -174,6 +176,20 @@ public final class ClientSession implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the stream starts like our protocol.
|
||||||
|
*
|
||||||
|
* Our frames start with the magic "RSX1". The vanilla status question
|
||||||
|
* starts with something else, and the two are told apart by the first
|
||||||
|
* byte, so a port can serve both without waiting for the whole frame.
|
||||||
|
*/
|
||||||
|
private static boolean looksLikeOurs(byte[] chunk, int read) {
|
||||||
|
if (read <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (chunk[0] & 0xFF) == (net.rustnotmc.protocol.Frame.MAGIC >>> 24);
|
||||||
|
}
|
||||||
|
|
||||||
private void step() throws IOException {
|
private void step() throws IOException {
|
||||||
if (expired()) {
|
if (expired()) {
|
||||||
throw new ProtocolException("handshake took too long");
|
throw new ProtocolException("handshake took too long");
|
||||||
@@ -186,10 +202,22 @@ public final class ClientSession implements Runnable {
|
|||||||
this.lastError = "peer closed the connection";
|
this.lastError = "peer closed the connection";
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
if (this.firstLook) {
|
||||||
|
this.firstLook = false;
|
||||||
|
System.out.println("[DBG] first read=" + read + " b0=" + (read > 0 ? Integer.toHexString(chunk[0] & 0xFF) : "-"));
|
||||||
|
if (!looksLikeOurs(chunk, read)) {
|
||||||
|
// Not our protocol: the other side is asking about
|
||||||
|
// the server, so the server answers that question.
|
||||||
|
this.server.status().serve(this.socket, chunk, 0, read, this.address);
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.feed(chunk, 0, read);
|
this.feed(chunk, 0, read);
|
||||||
} catch (SocketTimeoutException nothingYet) {
|
} catch (SocketTimeoutException nothingYet) {
|
||||||
// expected: the loop wakes regularly to check the deadline
|
// expected: the loop wakes regularly to check the deadline
|
||||||
|
System.out.println("[DBG] step timeout");
|
||||||
} catch (SocketException gone) {
|
} catch (SocketException gone) {
|
||||||
this.lastError = String.valueOf(gone.getMessage());
|
this.lastError = String.valueOf(gone.getMessage());
|
||||||
close();
|
close();
|
||||||
@@ -340,17 +368,17 @@ public final class ClientSession implements Runnable {
|
|||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
TokenAuth checker = this.server.tokenAuth();
|
TokenAuth checker = this.server.tokenAuth();
|
||||||
if (checker != null) {
|
if (checker != null) {
|
||||||
net.rustnotmc.auth.GameToken.Claims claims =
|
TokenAuth.Verdict verdict = checker.verdict(given.uuid, given.token, now);
|
||||||
checker.check(given.uuid, given.token, now);
|
if (!verdict.accepted()) {
|
||||||
if (claims == null) {
|
|
||||||
this.server.log("token of " + given.account + " refused: "
|
this.server.log("token of " + given.account + " refused: "
|
||||||
+ "it is not ours, it is for another server, or it has run out");
|
+ verdict.refusal.reason);
|
||||||
this.server.throttle().failed(given.account, this.address);
|
this.server.throttle().failed(given.account, this.address);
|
||||||
send(Packets.AUTH_ACK, Packets.AuthAck.refused(
|
send(Packets.AUTH_ACK, Packets.AuthAck.refused(
|
||||||
"the session is not valid, please sign in again"));
|
"the session was refused: " + verdict.refusal.reason));
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
net.rustnotmc.auth.GameToken.Claims claims = verdict.claims;
|
||||||
|
|
||||||
// The name is inside the token, and the uuid has to be the same
|
// The name is inside the token, and the uuid has to be the same
|
||||||
// one the token was made for.
|
// one the token was made for.
|
||||||
@@ -372,11 +400,20 @@ public final class ClientSession implements Runnable {
|
|||||||
// launcher.
|
// launcher.
|
||||||
net.rustnotmc.auth.TokenService.Token own =
|
net.rustnotmc.auth.TokenService.Token own =
|
||||||
this.server.tokens().check(given.token);
|
this.server.tokens().check(given.token);
|
||||||
if (own == null || own.expired(now)) {
|
if (own == null) {
|
||||||
this.server.log("token of " + given.account + " refused: it is not ours");
|
this.server.log("token of " + given.account + " refused: "
|
||||||
|
+ "this server did not give it out");
|
||||||
this.server.throttle().failed(given.account, this.address);
|
this.server.throttle().failed(given.account, this.address);
|
||||||
send(Packets.AUTH_ACK, Packets.AuthAck.refused(
|
send(Packets.AUTH_ACK, Packets.AuthAck.refused(
|
||||||
"the session is not valid, please sign in again"));
|
"the session was refused: this server did not give out that token"));
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (own.expired(now)) {
|
||||||
|
this.server.log("token of " + given.account + " refused: it has run out");
|
||||||
|
this.server.throttle().failed(given.account, this.address);
|
||||||
|
send(Packets.AUTH_ACK, Packets.AuthAck.refused(
|
||||||
|
"the session was refused: the token has run out"));
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import net.rustnotmc.server.world.World;
|
|||||||
* depend on the graphics card, and the whole point of moving the simulation to
|
* depend on the graphics card, and the whole point of moving the simulation to
|
||||||
* our own server is that the client stops being trusted.
|
* our own server is that the client stops being trusted.
|
||||||
*/
|
*/
|
||||||
public final class Server {
|
public final class Server implements net.rustnotmc.server.status.StatusSession.Info {
|
||||||
|
|
||||||
/** Ticks per second. Fixed, and the same number the client counts. */
|
/** Ticks per second. Fixed, and the same number the client counts. */
|
||||||
public static final int TICKS_PER_SECOND = 20;
|
public static final int TICKS_PER_SECOND = 20;
|
||||||
@@ -63,6 +63,10 @@ public final class Server {
|
|||||||
new net.rustnotmc.server.player.PlayerData(null);
|
new net.rustnotmc.server.player.PlayerData(null);
|
||||||
private volatile net.rustnotmc.server.world.LodGenerator lod;
|
private volatile net.rustnotmc.server.world.LodGenerator lod;
|
||||||
|
|
||||||
|
/** The status of the server, in the way the game asks for it. */
|
||||||
|
private final net.rustnotmc.server.status.StatusSession status =
|
||||||
|
new net.rustnotmc.server.status.StatusSession(this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The address the server listens on.
|
* The address the server listens on.
|
||||||
*
|
*
|
||||||
@@ -275,6 +279,11 @@ public final class Server {
|
|||||||
this.bindAddress = address;
|
this.bindAddress = address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The status of the server, in the way the game asks for it. */
|
||||||
|
public net.rustnotmc.server.status.StatusSession status() {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
/** The far view of the world. */
|
/** The far view of the world. */
|
||||||
public net.rustnotmc.server.world.LodGenerator lod() {
|
public net.rustnotmc.server.world.LodGenerator lod() {
|
||||||
return this.lod;
|
return this.lod;
|
||||||
@@ -354,6 +363,17 @@ public final class Server {
|
|||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return this.serverName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String version() {
|
||||||
|
return "RustNotMC 1.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String motd() {
|
public String motd() {
|
||||||
return this.motd;
|
return this.motd;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,83 @@ public final class TokenAuth {
|
|||||||
return this.audience;
|
return this.audience;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Why a token was not taken. */
|
||||||
|
public enum Refusal {
|
||||||
|
|
||||||
|
/** Nothing is wrong with the token. */
|
||||||
|
NONE(""),
|
||||||
|
|
||||||
|
/** The signature does not match: the token is not from this server. */
|
||||||
|
NOT_SIGNED("the token is not signed by this server"),
|
||||||
|
|
||||||
|
/** The token was made for another server. */
|
||||||
|
WRONG_AUDIENCE("the token is meant for another server"),
|
||||||
|
|
||||||
|
/** The token is a reload token, not a session token. */
|
||||||
|
WRONG_KIND("that token is not a session token"),
|
||||||
|
|
||||||
|
/** The token has run out. */
|
||||||
|
EXPIRED("the token has run out"),
|
||||||
|
|
||||||
|
/** The token was made for another player. */
|
||||||
|
WRONG_PLAYER("the token belongs to another player");
|
||||||
|
|
||||||
|
/** What to tell the player. */
|
||||||
|
public final String reason;
|
||||||
|
|
||||||
|
Refusal(String reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The answer to a token: what it says, or why it was refused. */
|
||||||
|
public static final class Verdict {
|
||||||
|
|
||||||
|
/** What the token says, or null when it was refused. */
|
||||||
|
public final GameToken.Claims claims;
|
||||||
|
|
||||||
|
/** Why the token was refused. */
|
||||||
|
public final Refusal refusal;
|
||||||
|
|
||||||
|
Verdict(GameToken.Claims claims, Refusal refusal) {
|
||||||
|
this.claims = claims;
|
||||||
|
this.refusal = refusal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the token was taken. */
|
||||||
|
public boolean accepted() {
|
||||||
|
return this.claims != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a token and says what was wrong.
|
||||||
|
*
|
||||||
|
* The reason matters: "the token is not ours" tells a player nothing, and
|
||||||
|
* a player who cannot tell a stale token from a foreign one cannot fix
|
||||||
|
* anything either.
|
||||||
|
*/
|
||||||
|
public Verdict verdict(String uuid, String token, long now) {
|
||||||
|
GameToken.Claims claims = this.tokens.read(token);
|
||||||
|
if (claims == null) {
|
||||||
|
return new Verdict(null, Refusal.NOT_SIGNED);
|
||||||
|
}
|
||||||
|
if (!GameToken.KIND_SESSION.equals(claims.kind)) {
|
||||||
|
return new Verdict(null, Refusal.WRONG_KIND);
|
||||||
|
}
|
||||||
|
if (!this.audience.equals(claims.audience)) {
|
||||||
|
return new Verdict(null, Refusal.WRONG_AUDIENCE);
|
||||||
|
}
|
||||||
|
if (claims.expiresAt <= now) {
|
||||||
|
return new Verdict(null, Refusal.EXPIRED);
|
||||||
|
}
|
||||||
|
if (uuid != null && !uuid.isEmpty() && claims.uuid != null
|
||||||
|
&& !uuid.equals(claims.uuid)) {
|
||||||
|
return new Verdict(null, Refusal.WRONG_PLAYER);
|
||||||
|
}
|
||||||
|
return new Verdict(claims, Refusal.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks a token.
|
* Checks a token.
|
||||||
*
|
*
|
||||||
@@ -49,24 +126,7 @@ public final class TokenAuth {
|
|||||||
* caller tells the player what was wrong.
|
* caller tells the player what was wrong.
|
||||||
*/
|
*/
|
||||||
public GameToken.Claims check(String uuid, String token, long now) {
|
public GameToken.Claims check(String uuid, String token, long now) {
|
||||||
GameToken.Claims claims = this.tokens.read(token);
|
return verdict(uuid, token, now).claims;
|
||||||
if (claims == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!GameToken.KIND_SESSION.equals(claims.kind)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!this.audience.equals(claims.audience)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (claims.expiresAt <= now) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (uuid != null && !uuid.isEmpty() && claims.uuid != null
|
|
||||||
&& !uuid.equals(claims.uuid)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return claims;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package net.rustnotmc.server.status;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of the server, in the way the game asks for it.
|
||||||
|
*
|
||||||
|
* The vanilla client checks a server before it lets anybody in: it sends a
|
||||||
|
* handshake, then a request, and waits for an answer that says how the server
|
||||||
|
* calls itself, how many players are on it and how many may come. The answer
|
||||||
|
* is a small piece of text in a shape everybody knows.
|
||||||
|
*
|
||||||
|
* Our own protocol has a magic of its own, so a port can tell at once that it
|
||||||
|
* is being spoken nonsense. But a server that cannot be seen is a server that
|
||||||
|
* does not exist for half of the tools around, so the port answers the
|
||||||
|
* vanilla question too: the first bytes say which of the two conversations
|
||||||
|
* this is.
|
||||||
|
*
|
||||||
|
* The vanilla client cannot join through this door: the game speaks our
|
||||||
|
* protocol, not the vanilla one. It is told so in words instead of being cut
|
||||||
|
* off in the middle of a handshake.
|
||||||
|
*/
|
||||||
|
public final class StatusSession {
|
||||||
|
|
||||||
|
/** The packet id of a handshake in the vanilla protocol. */
|
||||||
|
private static final int VANILLA_HANDSHAKE = 0x00;
|
||||||
|
|
||||||
|
/** The packet id of a status request in the vanilla protocol. */
|
||||||
|
private static final int VANILLA_REQUEST = 0x00;
|
||||||
|
|
||||||
|
/** The packet id of a ping in the vanilla protocol. */
|
||||||
|
private static final int VANILLA_PING = 0x01;
|
||||||
|
|
||||||
|
/** The byte a legacy ping starts with. */
|
||||||
|
private static final int LEGACY_PING = 0xFE;
|
||||||
|
|
||||||
|
/** The byte a legacy answer starts with. */
|
||||||
|
private static final int LEGACY_ANSWER = 0xFF;
|
||||||
|
|
||||||
|
/** The state of the vanilla protocol that means "the status is wanted". */
|
||||||
|
private static final int STATE_STATUS = 1;
|
||||||
|
|
||||||
|
/** The state of the vanilla protocol that means "somebody wants in". */
|
||||||
|
private static final int STATE_LOGIN = 2;
|
||||||
|
|
||||||
|
/** How long the answer may take. */
|
||||||
|
private static final int TIMEOUT_MILLIS = 4000;
|
||||||
|
|
||||||
|
/** What the server says about itself. */
|
||||||
|
public interface Info {
|
||||||
|
|
||||||
|
/** The name of the server. */
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/** The line under the name. */
|
||||||
|
String motd();
|
||||||
|
|
||||||
|
/** How many players are on the server right now. */
|
||||||
|
int online();
|
||||||
|
|
||||||
|
/** How many players may come. */
|
||||||
|
int capacity();
|
||||||
|
|
||||||
|
/** The version the server calls itself. */
|
||||||
|
String version();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Info info;
|
||||||
|
|
||||||
|
public StatusSession(Info info) {
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answers one status conversation.
|
||||||
|
*
|
||||||
|
* The bytes that were already read are handed over, so that nothing is
|
||||||
|
* lost between the first look and the rest of the stream.
|
||||||
|
*/
|
||||||
|
public void serve(Socket socket, byte[] head, int offset, int length, String address) {
|
||||||
|
try {
|
||||||
|
socket.setSoTimeout(TIMEOUT_MILLIS);
|
||||||
|
InputStream in = socket.getInputStream();
|
||||||
|
OutputStream out = socket.getOutputStream();
|
||||||
|
|
||||||
|
// The first byte says which of the two conversations this is.
|
||||||
|
int first;
|
||||||
|
if (length > 0) {
|
||||||
|
first = head[offset] & 0xFF;
|
||||||
|
} else {
|
||||||
|
first = in.read();
|
||||||
|
if (first < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Reader reader = new Reader(head, offset, length, in, first);
|
||||||
|
if (first == LEGACY_PING) {
|
||||||
|
// the marker is read; the rest of the old packet is not
|
||||||
|
reader.skipPending();
|
||||||
|
legacy(reader, out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modern(reader, out);
|
||||||
|
} catch (IOException broken) {
|
||||||
|
// the other side went away; nothing to say about it
|
||||||
|
} catch (RuntimeException broken) {
|
||||||
|
// a broken status request must never take the server down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The old way of asking: one packet, one answer. */
|
||||||
|
private void legacy(Reader reader, OutputStream out) throws IOException {
|
||||||
|
reader.readUnsignedByte(); // 0x01
|
||||||
|
reader.readUnsignedByte(); // 0xFA
|
||||||
|
int length = reader.readUnsignedByte(); // length of the rest
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
reader.readUnsignedByte();
|
||||||
|
}
|
||||||
|
String text = "§1\000" + vanillaProtocol() + "\000" + this.info.version()
|
||||||
|
+ "\000" + this.info.motd() + "\000" + this.info.online()
|
||||||
|
+ "\000" + this.info.capacity();
|
||||||
|
byte[] body = text.getBytes(StandardCharsets.UTF_8);
|
||||||
|
out.write(LEGACY_ANSWER);
|
||||||
|
out.write((body.length >> 8) & 0xFF);
|
||||||
|
out.write(body.length & 0xFF);
|
||||||
|
out.write(body);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The way the game asks today: a handshake, a request, a ping. */
|
||||||
|
private void modern(Reader reader, OutputStream out) throws IOException {
|
||||||
|
int packetId = reader.readUnsignedByte();
|
||||||
|
if (packetId != VANILLA_HANDSHAKE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reader.readVarInt(); // the protocol version of the other side
|
||||||
|
reader.readString(); // the name the player typed
|
||||||
|
reader.readShort(); // the port
|
||||||
|
int state = reader.readVarInt();
|
||||||
|
|
||||||
|
if (state == STATE_LOGIN) {
|
||||||
|
// Somebody wants in through the vanilla door. They cannot come in
|
||||||
|
// that way, but they can be told why in words they will see.
|
||||||
|
drain(reader, 512);
|
||||||
|
String reason = "{\"text\":\"This is a RustNotMC server. "
|
||||||
|
+ "It is played with the RustNotMC client, not with the vanilla one.\"}";
|
||||||
|
byte[] body = reason.getBytes(StandardCharsets.UTF_8);
|
||||||
|
out.write(0x00);
|
||||||
|
writeVarInt(out, body.length);
|
||||||
|
out.write(body);
|
||||||
|
out.flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state != STATE_STATUS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The answer goes out right away: the client asks for the status and
|
||||||
|
// for the latency at the same time, and the latency is a ping that
|
||||||
|
// comes after the answer.
|
||||||
|
byte[] json = statusJson().getBytes(StandardCharsets.UTF_8);
|
||||||
|
out.write(VANILLA_REQUEST);
|
||||||
|
writeVarInt(out, json.length);
|
||||||
|
out.write(json);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
// and now the ping, if it comes. The request itself is of no use
|
||||||
|
// here, so it is passed over.
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
int next = reader.readUnsignedByte();
|
||||||
|
if (next != VANILLA_PING) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long stamp = 0L;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
stamp = (stamp << 8) | reader.readUnsignedByte();
|
||||||
|
}
|
||||||
|
out.write(VANILLA_PING);
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
out.write((int) ((stamp >>> (8 * (7 - i))) & 0xFFL));
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (IOException nothingMore) {
|
||||||
|
// the client did not want the latency; the status was enough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The status, in the shape the game expects. */
|
||||||
|
public String statusJson() {
|
||||||
|
StringBuilder out = new StringBuilder(256);
|
||||||
|
out.append("{\"version\":{\"name\":")
|
||||||
|
.append(net.rustnotmc.server.util.Json.quote(this.info.version()))
|
||||||
|
.append(",\"protocol\":").append(vanillaProtocol())
|
||||||
|
.append("},\"players\":{\"max\":").append(this.info.capacity())
|
||||||
|
.append(",\"online\":").append(this.info.online())
|
||||||
|
.append(",\"sample\":[]}")
|
||||||
|
.append(",\"description\":{\"text\":")
|
||||||
|
.append(net.rustnotmc.server.util.Json.quote(this.info.motd()))
|
||||||
|
.append("}}");
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The protocol number the game knows.
|
||||||
|
*
|
||||||
|
* The server does not speak the vanilla protocol, but the number is what
|
||||||
|
* the tools that list servers show next to the name.
|
||||||
|
*/
|
||||||
|
private int vanillaProtocol() {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads what is there and throws the rest away. */
|
||||||
|
private static void drain(Reader reader, int limit) {
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < limit; i++) {
|
||||||
|
reader.readUnsignedByte();
|
||||||
|
}
|
||||||
|
} catch (IOException nothingMore) {
|
||||||
|
// there is nothing more to read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes a varint the way the game does: seven bits at a time. */
|
||||||
|
private static void writeVarInt(OutputStream out, int value) throws IOException {
|
||||||
|
int left = value;
|
||||||
|
while (true) {
|
||||||
|
int part = left & 0x7F;
|
||||||
|
left >>>= 7;
|
||||||
|
if (left != 0) {
|
||||||
|
part |= 0x80;
|
||||||
|
}
|
||||||
|
out.write(part);
|
||||||
|
if (left == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the little pieces of the vanilla protocol.
|
||||||
|
*
|
||||||
|
* The bytes that were already read from the socket come first, and the
|
||||||
|
* rest is taken from the stream.
|
||||||
|
*/
|
||||||
|
private static final class Reader {
|
||||||
|
|
||||||
|
/** The bytes that were read already. */
|
||||||
|
private final byte[] head;
|
||||||
|
|
||||||
|
/** The next byte of those to read. */
|
||||||
|
private int pos;
|
||||||
|
|
||||||
|
/** Where the bytes that were read already end. */
|
||||||
|
private final int limit;
|
||||||
|
|
||||||
|
/** The rest of the stream. */
|
||||||
|
private final InputStream in;
|
||||||
|
|
||||||
|
/** A byte that was read before the reader was made. */
|
||||||
|
private int pending;
|
||||||
|
|
||||||
|
Reader(byte[] head, int offset, int length, InputStream in, int pending) {
|
||||||
|
this.head = head;
|
||||||
|
// When the first byte came out of the head, the head still holds a
|
||||||
|
// copy of it that was already counted in `pending` (it is read
|
||||||
|
// again from `pending`, not from the head).
|
||||||
|
this.pos = length > 0 ? offset + 1 : offset;
|
||||||
|
this.limit = offset + length;
|
||||||
|
this.in = in;
|
||||||
|
this.pending = pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throws away a byte that was read before the reader was made. */
|
||||||
|
void skipPending() {
|
||||||
|
this.pending = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int readUnsignedByte() throws IOException {
|
||||||
|
if (this.pending >= 0) {
|
||||||
|
int b = this.pending;
|
||||||
|
this.pending = -1;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if (this.pos < this.limit) {
|
||||||
|
return this.head[this.pos++] & 0xFF;
|
||||||
|
}
|
||||||
|
int b = this.in.read();
|
||||||
|
if (b < 0) {
|
||||||
|
throw new IOException("the other side closed the connection");
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
int readVarInt() throws IOException {
|
||||||
|
int result = 0;
|
||||||
|
int shift = 0;
|
||||||
|
while (true) {
|
||||||
|
int b = readUnsignedByte();
|
||||||
|
result |= (b & 0x7F) << shift;
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
shift += 7;
|
||||||
|
if (shift > 35) {
|
||||||
|
throw new IOException("the varint is too long");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String readString() throws IOException {
|
||||||
|
int length = readVarInt();
|
||||||
|
byte[] bytes = new byte[length];
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
bytes[i] = (byte) readUnsignedByte();
|
||||||
|
}
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
int readShort() throws IOException {
|
||||||
|
int a = readUnsignedByte();
|
||||||
|
int b = readUnsignedByte();
|
||||||
|
return (a << 8) | b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package net.rustnotmc.server.status;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import net.rustnotmc.server.Server;
|
||||||
|
import net.rustnotmc.server.world.ChunkStorage;
|
||||||
|
import net.rustnotmc.server.world.World;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the status question the way it really arrives: the bytes go to a live
|
||||||
|
* server, a session takes them and hands them over to the status.
|
||||||
|
*/
|
||||||
|
public final class StatusRouteTest {
|
||||||
|
|
||||||
|
private static int checks;
|
||||||
|
private static int failed;
|
||||||
|
private static final java.util.List<String> failures = new java.util.ArrayList<String>();
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Path work = Files.createTempDirectory("status-route");
|
||||||
|
Server server = StatusRouteTest.boot(work);
|
||||||
|
try {
|
||||||
|
legacy(server.port());
|
||||||
|
modern(server.port());
|
||||||
|
} finally {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
for (int i = 0; i < failures.size(); i++) {
|
||||||
|
System.out.println(" FAIL " + failures.get(i));
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("checks: " + checks + ", failed: " + failed);
|
||||||
|
if (failed != 0) {
|
||||||
|
System.out.println("FAILED");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
System.out.println("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A small server that only has to answer the question of the status. */
|
||||||
|
private static Server boot(Path work) throws Exception {
|
||||||
|
Server server = new Server("RustNotMC route", "hello", 200, 0,
|
||||||
|
new World(1234L, ChunkStorage.memoryOnly()));
|
||||||
|
Path accounts = work.resolve("route-accounts.txt");
|
||||||
|
server.loadAccounts(accounts);
|
||||||
|
server.start();
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface Throwing {
|
||||||
|
void go() throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(String name, Throwing body) {
|
||||||
|
checks++;
|
||||||
|
int before = failed;
|
||||||
|
try {
|
||||||
|
body.go();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failed++;
|
||||||
|
failures.add(name + ": threw " + t);
|
||||||
|
t.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failed == before) {
|
||||||
|
System.out.println(" ok " + name);
|
||||||
|
} else {
|
||||||
|
System.out.println(" FAIL " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void eq(Object what, Object expected, Object actual) {
|
||||||
|
if (expected == null ? actual != null : !expected.equals(actual)) {
|
||||||
|
failed++;
|
||||||
|
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void yes(String what, boolean condition) {
|
||||||
|
if (!condition) {
|
||||||
|
failed++;
|
||||||
|
failures.add(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void legacy(int port) throws Exception {
|
||||||
|
run("the old question is answered on a live port", new Throwing() {
|
||||||
|
public void go() throws Exception {
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", port)) {
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
DataOutputStream out = new DataOutputStream(
|
||||||
|
socket.getOutputStream());
|
||||||
|
out.writeByte(0xFE);
|
||||||
|
out.writeByte(0x01);
|
||||||
|
out.writeByte(0xFA);
|
||||||
|
out.writeByte(0x01);
|
||||||
|
out.writeByte(0x00);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(
|
||||||
|
socket.getInputStream());
|
||||||
|
eq("the answer is a legacy one", Integer.valueOf(0xFF),
|
||||||
|
Integer.valueOf(in.readUnsignedByte()));
|
||||||
|
int length = in.readUnsignedShort();
|
||||||
|
byte[] body = new byte[length];
|
||||||
|
in.readFully(body);
|
||||||
|
String text = new String(body,
|
||||||
|
java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
yes("the answer says the players", text.contains("2"));
|
||||||
|
yes("the answer says the places", text.contains("200"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void modern(int port) throws Exception {
|
||||||
|
run("the new question is answered on a live port", new Throwing() {
|
||||||
|
public void go() throws Exception {
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", port)) {
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
DataOutputStream out = new DataOutputStream(
|
||||||
|
socket.getOutputStream());
|
||||||
|
out.writeByte(0x00); // the handshake packet
|
||||||
|
writeVarInt(out, 404); // the protocol version
|
||||||
|
writeString(out, "127.0.0.1");
|
||||||
|
out.writeShort(port);
|
||||||
|
writeVarInt(out, 1); // the status is wanted
|
||||||
|
out.writeByte(0x00); // the request
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(
|
||||||
|
socket.getInputStream());
|
||||||
|
eq("the answer is a status", Integer.valueOf(0x00),
|
||||||
|
Integer.valueOf(in.readUnsignedByte()));
|
||||||
|
String json = readString(in);
|
||||||
|
yes("the answer names the server", json.contains("hello"));
|
||||||
|
yes("the answer says the players", json.contains("\"online\":0"));
|
||||||
|
|
||||||
|
long mark = 0x0102030405060708L;
|
||||||
|
out.writeByte(0x01);
|
||||||
|
out.writeLong(mark);
|
||||||
|
out.flush();
|
||||||
|
eq("the answer is a pong", Integer.valueOf(0x01),
|
||||||
|
Integer.valueOf(in.readUnsignedByte()));
|
||||||
|
eq("the mark comes back", Long.valueOf(mark),
|
||||||
|
Long.valueOf(in.readLong()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
run("a game in vanilla clothes is turned away in words", new Throwing() {
|
||||||
|
public void go() throws Exception {
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", port)) {
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
DataOutputStream out = new DataOutputStream(
|
||||||
|
socket.getOutputStream());
|
||||||
|
out.writeByte(0x00); // the handshake packet
|
||||||
|
writeVarInt(out, 404);
|
||||||
|
writeString(out, "127.0.0.1");
|
||||||
|
out.writeShort(port);
|
||||||
|
writeVarInt(out, 2); // somebody wants in
|
||||||
|
out.writeByte(0x00); // the login packet
|
||||||
|
writeString(out, "Player");
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(
|
||||||
|
socket.getInputStream());
|
||||||
|
eq("the answer is a disconnect", Integer.valueOf(0x00),
|
||||||
|
Integer.valueOf(in.readUnsignedByte()));
|
||||||
|
String reason = readString(in);
|
||||||
|
yes("the reason says what to use", reason.contains("RustNotMC"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeVarInt(DataOutputStream out, int value) throws IOException {
|
||||||
|
int left = value;
|
||||||
|
while (true) {
|
||||||
|
int part = left & 0x7F;
|
||||||
|
left >>>= 7;
|
||||||
|
if (left != 0) {
|
||||||
|
part |= 0x80;
|
||||||
|
}
|
||||||
|
out.writeByte(part);
|
||||||
|
if (left == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeString(DataOutputStream out, String value) throws IOException {
|
||||||
|
byte[] bytes = value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
writeVarInt(out, bytes.length);
|
||||||
|
out.write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readString(DataInputStream in) throws IOException {
|
||||||
|
int length = 0;
|
||||||
|
int shift = 0;
|
||||||
|
while (true) {
|
||||||
|
int b = in.readByte() & 0xFF;
|
||||||
|
length |= (b & 0x7F) << shift;
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
shift += 7;
|
||||||
|
}
|
||||||
|
byte[] bytes = new byte[length];
|
||||||
|
in.readFully(bytes);
|
||||||
|
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
package net.rustnotmc.server.status;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the answer to the vanilla status question.
|
||||||
|
*
|
||||||
|
* The game checks a server before it lets anybody in, and the tools that list
|
||||||
|
* servers do the same. The server has to answer that question on the same
|
||||||
|
* port it plays on, and the answer has to say how the server calls itself,
|
||||||
|
* how many players are on it and how many may come.
|
||||||
|
*/
|
||||||
|
public final class StatusTest {
|
||||||
|
|
||||||
|
private static int checks;
|
||||||
|
private static int failed;
|
||||||
|
private static final java.util.List<String> failures = new java.util.ArrayList<String>();
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
run("the status says who the server is", new Throwing() {
|
||||||
|
public void go() throws Exception { testJson(); }
|
||||||
|
});
|
||||||
|
run("the vanilla question is answered", new Throwing() {
|
||||||
|
public void go() throws Exception { testModern(); }
|
||||||
|
});
|
||||||
|
run("the latency is measured", new Throwing() {
|
||||||
|
public void go() throws Exception { testPing(); }
|
||||||
|
});
|
||||||
|
run("the old way of asking is answered too", new Throwing() {
|
||||||
|
public void go() throws Exception { testLegacy(); }
|
||||||
|
});
|
||||||
|
run("a vanilla login is turned away in words", new Throwing() {
|
||||||
|
public void go() throws Exception { testLogin(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
for (int i = 0; i < failures.size(); i++) {
|
||||||
|
System.out.println(" FAIL " + failures.get(i));
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("checks: " + checks + ", failed: " + failed);
|
||||||
|
if (failed != 0) {
|
||||||
|
System.out.println("FAILED");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
System.out.println("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface Throwing {
|
||||||
|
void go() throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(String name, Throwing body) {
|
||||||
|
checks++;
|
||||||
|
int before = failed;
|
||||||
|
try {
|
||||||
|
body.go();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failed++;
|
||||||
|
failures.add(name + ": threw " + t);
|
||||||
|
t.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failed == before) {
|
||||||
|
System.out.println(" ok " + name);
|
||||||
|
} else {
|
||||||
|
System.out.println(" FAIL " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void eq(Object what, Object expected, Object actual) {
|
||||||
|
if (expected == null ? actual != null : !expected.equals(actual)) {
|
||||||
|
failed++;
|
||||||
|
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void yes(String what, boolean condition) {
|
||||||
|
if (!condition) {
|
||||||
|
failed++;
|
||||||
|
failures.add(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A server that calls itself one thing and holds two players. */
|
||||||
|
private static final class Fake implements StatusSession.Info {
|
||||||
|
public String name() {
|
||||||
|
return "RustNotMC";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String motd() {
|
||||||
|
return "an island in the sea";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int online() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int capacity() {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String version() {
|
||||||
|
return "RustNotMC 1.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testJson() {
|
||||||
|
String json = new StatusSession(new Fake()).statusJson();
|
||||||
|
yes("the name is in there", json.contains("\"RustNotMC 1.0\""));
|
||||||
|
yes("the line under the name is in there", json.contains("an island in the sea"));
|
||||||
|
yes("the players are in there", json.contains("\"online\":2"));
|
||||||
|
yes("the places are in there", json.contains("\"max\":200"));
|
||||||
|
yes("it is one piece of text", json.startsWith("{") && json.endsWith("}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testModern() throws Exception {
|
||||||
|
try (ServerSocket listener = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))) {
|
||||||
|
Thread server = new Thread(() -> {
|
||||||
|
try (Socket socket = listener.accept()) {
|
||||||
|
new StatusSession(new Fake()).serve(socket,
|
||||||
|
new byte[0], 0, 0, "127.0.0.1");
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// the test is over anyway
|
||||||
|
}
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", listener.getLocalPort())) {
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
out.writeByte(0x00); // the handshake
|
||||||
|
writeVarInt(out, 404); // the protocol version
|
||||||
|
writeString(out, "127.0.0.1");
|
||||||
|
out.writeShort(listener.getLocalPort());
|
||||||
|
writeVarInt(out, 1); // the status is wanted
|
||||||
|
out.writeByte(0x00); // the request
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||||
|
eq("the answer is a status", Integer.valueOf(0x00), Integer.valueOf(in.readByte()));
|
||||||
|
String json = readString(in);
|
||||||
|
yes("the answer names the server", json.contains("RustNotMC 1.0"));
|
||||||
|
yes("the answer says how many players", json.contains("\"online\":2"));
|
||||||
|
}
|
||||||
|
server.join(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testPing() throws Exception {
|
||||||
|
try (ServerSocket listener = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))) {
|
||||||
|
Thread server = new Thread(() -> {
|
||||||
|
try (Socket socket = listener.accept()) {
|
||||||
|
new StatusSession(new Fake()).serve(socket,
|
||||||
|
new byte[0], 0, 0, "127.0.0.1");
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// the test is over anyway
|
||||||
|
}
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", listener.getLocalPort())) {
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
out.writeByte(0x00);
|
||||||
|
writeVarInt(out, 404);
|
||||||
|
writeString(out, "127.0.0.1");
|
||||||
|
out.writeShort(listener.getLocalPort());
|
||||||
|
writeVarInt(out, 1);
|
||||||
|
out.writeByte(0x00);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||||
|
in.readByte();
|
||||||
|
readString(in);
|
||||||
|
|
||||||
|
// and now the ping, with a mark of its own
|
||||||
|
long mark = 0x0102030405060708L;
|
||||||
|
out.writeByte(0x01);
|
||||||
|
out.writeLong(mark);
|
||||||
|
out.flush();
|
||||||
|
eq("the answer is a pong", Integer.valueOf(0x01), Integer.valueOf(in.readByte()));
|
||||||
|
eq("the mark comes back", Long.valueOf(mark), Long.valueOf(in.readLong()));
|
||||||
|
}
|
||||||
|
server.join(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testLegacy() throws Exception {
|
||||||
|
try (ServerSocket listener = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))) {
|
||||||
|
Thread server = new Thread(() -> {
|
||||||
|
try (Socket socket = listener.accept()) {
|
||||||
|
new StatusSession(new Fake()).serve(socket,
|
||||||
|
new byte[0], 0, 0, "127.0.0.1");
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
ignored.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", listener.getLocalPort())) {
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
out.writeByte(0xFE);
|
||||||
|
out.writeByte(0x01);
|
||||||
|
out.writeByte(0xFA);
|
||||||
|
out.writeByte(0x01);
|
||||||
|
out.writeByte(0x00);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||||
|
eq("the answer is a legacy one", Integer.valueOf(0xFF),
|
||||||
|
Integer.valueOf(in.readUnsignedByte()));
|
||||||
|
int length = in.readUnsignedShort();
|
||||||
|
byte[] body = new byte[length];
|
||||||
|
in.readFully(body);
|
||||||
|
String text = new String(body, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
yes("the answer says how many players", text.contains("2"));
|
||||||
|
yes("the answer says how many places", text.contains("200"));
|
||||||
|
}
|
||||||
|
server.join(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testLogin() throws Exception {
|
||||||
|
try (ServerSocket listener = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))) {
|
||||||
|
Thread server = new Thread(() -> {
|
||||||
|
try (Socket socket = listener.accept()) {
|
||||||
|
new StatusSession(new Fake()).serve(socket,
|
||||||
|
new byte[0], 0, 0, "127.0.0.1");
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// the test is over anyway
|
||||||
|
}
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", listener.getLocalPort())) {
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
out.writeByte(0x00);
|
||||||
|
writeVarInt(out, 404);
|
||||||
|
writeString(out, "127.0.0.1");
|
||||||
|
out.writeShort(listener.getLocalPort());
|
||||||
|
writeVarInt(out, 2); // somebody wants in
|
||||||
|
out.writeByte(0x00); // the login
|
||||||
|
writeString(out, "Player");
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||||
|
eq("the answer is a disconnect", Integer.valueOf(0x00),
|
||||||
|
Integer.valueOf(in.readByte()));
|
||||||
|
String reason = readString(in);
|
||||||
|
yes("the reason says what to use", reason.contains("RustNotMC"));
|
||||||
|
}
|
||||||
|
server.join(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeVarInt(DataOutputStream out, int value) throws IOException {
|
||||||
|
int left = value;
|
||||||
|
while (true) {
|
||||||
|
int part = left & 0x7F;
|
||||||
|
left >>>= 7;
|
||||||
|
if (left != 0) {
|
||||||
|
part |= 0x80;
|
||||||
|
}
|
||||||
|
out.writeByte(part);
|
||||||
|
if (left == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeString(DataOutputStream out, String value) throws IOException {
|
||||||
|
byte[] bytes = value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
writeVarInt(out, bytes.length);
|
||||||
|
out.write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readString(DataInputStream in) throws IOException {
|
||||||
|
int length = 0;
|
||||||
|
int shift = 0;
|
||||||
|
while (true) {
|
||||||
|
int b = in.readByte() & 0xFF;
|
||||||
|
length |= (b & 0x7F) << shift;
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
shift += 7;
|
||||||
|
}
|
||||||
|
byte[] bytes = new byte[length];
|
||||||
|
in.readFully(bytes);
|
||||||
|
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-4
@@ -43,7 +43,10 @@ if ($line) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$tokenFile = Join-Path $Root 'rsx-server-core\world\dev-token.txt'
|
$tokenFile = Join-Path $Root 'rsx-server-core\world\dev-token.txt'
|
||||||
if ($Fresh -and (Test-Path $tokenFile)) { Remove-Item $tokenFile -Force }
|
# The server makes a new secret every time it starts, so the token of the
|
||||||
|
# last run is of no use to it. It has to go, or the client will be given a
|
||||||
|
# token the server does not know and will be turned away at the door.
|
||||||
|
if (Test-Path $tokenFile) { Remove-Item $tokenFile -Force }
|
||||||
|
|
||||||
Write-Host 'starting the server'
|
Write-Host 'starting the server'
|
||||||
$serverArgs = @(
|
$serverArgs = @(
|
||||||
@@ -51,7 +54,8 @@ $serverArgs = @(
|
|||||||
'--port', "$Port",
|
'--port', "$Port",
|
||||||
'--accounts', (Join-Path $Root 'rsx-server-core\accounts.txt'),
|
'--accounts', (Join-Path $Root 'rsx-server-core\accounts.txt'),
|
||||||
'--world', (Join-Path $Root 'rsx-server-core\world'),
|
'--world', (Join-Path $Root 'rsx-server-core\world'),
|
||||||
'--dev-token', $User
|
'--dev-token', $User,
|
||||||
|
'--bind', '127.0.0.1'
|
||||||
)
|
)
|
||||||
$java = 'D:\jdk-21\bin\java.exe'
|
$java = 'D:\jdk-21\bin\java.exe'
|
||||||
if (-not (Test-Path $java)) { $java = (Get-Command java).Source }
|
if (-not (Test-Path $java)) { $java = (Get-Command java).Source }
|
||||||
@@ -59,11 +63,15 @@ $server = Start-Process -FilePath $java -ArgumentList $serverArgs `
|
|||||||
-RedirectStandardOutput $ServerLog -RedirectStandardError "$ServerLog.err" -PassThru
|
-RedirectStandardOutput $ServerLog -RedirectStandardError "$ServerLog.err" -PassThru
|
||||||
Write-Host "server pid $($server.Id), log $ServerLog"
|
Write-Host "server pid $($server.Id), log $ServerLog"
|
||||||
|
|
||||||
# the server writes its own token, and the client needs it
|
# The server writes its own token, and the client needs it. The file has to be
|
||||||
|
# the one this run wrote, not the one of the run before.
|
||||||
$token = $null
|
$token = $null
|
||||||
for ($i = 0; $i -lt 40; $i++) {
|
$started = Get-Date
|
||||||
|
for ($i = 0; $i -lt 60; $i++) {
|
||||||
Start-Sleep -Milliseconds 500
|
Start-Sleep -Milliseconds 500
|
||||||
if (Test-Path $tokenFile) {
|
if (Test-Path $tokenFile) {
|
||||||
|
$written = (Get-Item $tokenFile).LastWriteTime
|
||||||
|
if ($written -lt $started) { continue }
|
||||||
$token = (Get-Content $tokenFile -Raw).Trim()
|
$token = (Get-Content $tokenFile -Raw).Trim()
|
||||||
if ($token) { break }
|
if ($token) { break }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user