Каркас протокола: кадрирование, буфер и рукопожатие
Это первый настоящий кусок сервера. Пока он ничего не умеет, кроме того что умеет правильно: принимать байты от чужого человека и не дать ему ничего сделать с нами. Формат кадра — четыре байта магии RSX1, версия, id пакета, длина тела и само тело. Магия стоит в начале, чтобы порт мог сразу ответить, а не ждать объяснений. Главное здесь — границы. Сервер живёт тем, что вынимает числа из сокетов чужих людей, и разница между дешёвой проверкой и аллокацией — это разница между «противник стоил нам пары тактов» и «противник стоил нам кучи». - Длина тела проверяется до того, как под неё что-то зарезервировано. Единственное, на что полезна вредная длина, — заставить сервер выделить память. - Varint ограничен пятью байтами для int и десятью для long. Без этого десять байт 0xFF превращают пятибайтовое целое в бесконечный цикл. - Длина строки ограничена до того, как под неё читают байты. - Декодер держит не больше одного максимального кадра плюс неполный заголовок. Неполные данные и испорченные — это разные вещи, и раньше это не было разделено: конверт, разрезанный сетью посреди varint, выглядел как испорченный и рвал соединение. IncompleteException означает «остальное уже в пути», ProtocolException — «потока больше не будет». TCP режет кадры почти всегда, так что первое поведение было бы отказом от большинства соединений. Позиция упакована одним varint, но доли у осей не равные: три поля по 26 бит это 78 бит, они не влезают в 64. X и Z получают по 26 бит с шагом 1/32 блока, Y — оставшиеся 12 бит целыми блоками. Со смещением на 2048 не вышло бы: для диапазона 0..4095 нужен тринадцатый бит, и это ровно тот бит, по которому значение читается обратно отрицательным. Первый прогон тестов нашёл все три ошибки сразу, две из них — в упаковке позиции. Проверено 17 сценариев, включая «по одному байту», пятьдесят кадров за одно чтение, чужую магию и объявленное тело в два гигабайта. Собирается и тестируется одним JDK, без Gradle и без сети, как и всё остальное в проекте.
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
package net.rustnotmc.protocol;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The packets that start a connection, and the table that maps them to ids.
|
||||
*
|
||||
* The ids are written down here rather than generated, because they end up in
|
||||
* every capture and every log line forever. Renumbering one is a protocol
|
||||
* change and should look like a deliberate act, not a side effect of moving a
|
||||
* class around.
|
||||
*
|
||||
* The order of a connection is fixed and short:
|
||||
*
|
||||
* <pre>
|
||||
* client -> Hello protocol version, what the client is
|
||||
* server -> HelloAck accepted version, server name, motd, online count
|
||||
* client -> Auth account and proof
|
||||
* server -> AuthAck session token, or a reason it was refused
|
||||
* client -> Login the session token
|
||||
* server -> LoginAck player id and entity id, or a reason
|
||||
* either -> Disconnect a human readable reason
|
||||
* </pre>
|
||||
*/
|
||||
public final class Packets {
|
||||
|
||||
private Packets() {
|
||||
}
|
||||
|
||||
/** Longest a name, motd or reason may be, in bytes. */
|
||||
public static final int MAX_TEXT = 512;
|
||||
|
||||
/** Longest a handshake proof may be, in bytes. */
|
||||
public static final int MAX_PROOF = 256;
|
||||
|
||||
/** Longest the handshake body may be, in bytes. */
|
||||
public static final int MAX_HANDSHAKE_BODY = 8 * 1024;
|
||||
|
||||
public static final int HELLO = 0x01;
|
||||
public static final int HELLO_ACK = 0x02;
|
||||
public static final int AUTH = 0x03;
|
||||
public static final int AUTH_ACK = 0x04;
|
||||
public static final int LOGIN = 0x05;
|
||||
public static final int LOGIN_ACK = 0x06;
|
||||
public static final int DISCONNECT = 0x7F;
|
||||
|
||||
/** The client introducing itself. */
|
||||
public static final class Hello {
|
||||
public final int protocolVersion;
|
||||
public final String clientName;
|
||||
public final int clientVersion;
|
||||
|
||||
public Hello(int protocolVersion, String clientName, int clientVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.clientName = clientName;
|
||||
this.clientVersion = clientVersion;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf();
|
||||
buf.writeVarInt(this.protocolVersion);
|
||||
buf.writeString(this.clientName);
|
||||
buf.writeVarInt(this.clientVersion);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static Hello read(Buf in) {
|
||||
return new Hello(in.readVarInt(), in.readString(MAX_TEXT), in.readVarInt());
|
||||
}
|
||||
}
|
||||
|
||||
/** The server accepting, and describing itself. */
|
||||
public static final class HelloAck {
|
||||
public final int protocolVersion;
|
||||
public final String serverName;
|
||||
public final String motd;
|
||||
public final int onlinePlayers;
|
||||
public final int maxPlayers;
|
||||
|
||||
public HelloAck(int protocolVersion, String serverName, String motd,
|
||||
int onlinePlayers, int maxPlayers) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.serverName = serverName;
|
||||
this.motd = motd;
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
this.maxPlayers = maxPlayers;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf();
|
||||
buf.writeVarInt(this.protocolVersion);
|
||||
buf.writeString(this.serverName);
|
||||
buf.writeString(this.motd);
|
||||
buf.writeVarInt(this.onlinePlayers);
|
||||
buf.writeVarInt(this.maxPlayers);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static HelloAck read(Buf in) {
|
||||
return new HelloAck(in.readVarInt(), in.readString(MAX_TEXT),
|
||||
in.readString(MAX_TEXT), in.readVarInt(), in.readVarInt());
|
||||
}
|
||||
}
|
||||
|
||||
/** The client claiming an account. */
|
||||
public static final class Auth {
|
||||
public final String account;
|
||||
public final String proof;
|
||||
|
||||
public Auth(String account, String proof) {
|
||||
this.account = account;
|
||||
this.proof = proof;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf();
|
||||
buf.writeString(this.account);
|
||||
buf.writeString(this.proof);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static Auth read(Buf in) {
|
||||
return new Auth(in.readString(MAX_TEXT), in.readString(MAX_PROOF));
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the account was accepted, and on what terms. */
|
||||
public static final class AuthAck {
|
||||
public final boolean accepted;
|
||||
public final String reason;
|
||||
public final String token;
|
||||
public final long expiresAt;
|
||||
|
||||
private AuthAck(boolean accepted, String reason, String token, long expiresAt) {
|
||||
this.accepted = accepted;
|
||||
this.reason = reason;
|
||||
this.token = token;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public static AuthAck ok(String token, long expiresAt) {
|
||||
return new AuthAck(true, "", token, expiresAt);
|
||||
}
|
||||
|
||||
public static AuthAck refused(String reason) {
|
||||
return new AuthAck(false, reason, "", 0L);
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf();
|
||||
buf.writeBoolean(this.accepted);
|
||||
buf.writeString(this.reason);
|
||||
if (this.accepted) {
|
||||
buf.writeString(this.token);
|
||||
buf.writeLong(this.expiresAt);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static AuthAck read(Buf in) {
|
||||
if (!in.readBoolean()) {
|
||||
return refused(in.readString(MAX_TEXT));
|
||||
}
|
||||
return ok(in.readString(MAX_TEXT), in.readLong());
|
||||
}
|
||||
}
|
||||
|
||||
/** The client redeeming the token it was just handed. */
|
||||
public static final class Login {
|
||||
public final String account;
|
||||
public final String token;
|
||||
|
||||
public Login(String account, String token) {
|
||||
this.account = account;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf();
|
||||
buf.writeString(this.account);
|
||||
buf.writeString(this.token);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static Login read(Buf in) {
|
||||
return new Login(in.readString(MAX_TEXT), in.readString(MAX_TEXT));
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the player is in. */
|
||||
public static final class LoginAck {
|
||||
public final boolean accepted;
|
||||
public final String reason;
|
||||
public final int playerId;
|
||||
public final int entityId;
|
||||
|
||||
private LoginAck(boolean accepted, String reason, int playerId, int entityId) {
|
||||
this.accepted = accepted;
|
||||
this.reason = reason;
|
||||
this.playerId = playerId;
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
public static LoginAck ok(int playerId, int entityId) {
|
||||
return new LoginAck(true, "", playerId, entityId);
|
||||
}
|
||||
|
||||
public static LoginAck refused(String reason) {
|
||||
return new LoginAck(false, reason, -1, -1);
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf();
|
||||
buf.writeBoolean(this.accepted);
|
||||
buf.writeString(this.reason);
|
||||
if (this.accepted) {
|
||||
buf.writeVarInt(this.playerId);
|
||||
buf.writeVarInt(this.entityId);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static LoginAck read(Buf in) {
|
||||
if (!in.readBoolean()) {
|
||||
return refused(in.readString(MAX_TEXT));
|
||||
}
|
||||
return ok(in.readVarInt(), in.readVarInt());
|
||||
}
|
||||
}
|
||||
|
||||
/** A reason, said out loud, before the connection goes away. */
|
||||
public static final class Disconnect {
|
||||
public final String reason;
|
||||
|
||||
public Disconnect(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
return new Buf().writeString(this.reason);
|
||||
}
|
||||
|
||||
public static Disconnect read(Buf in) {
|
||||
return new Disconnect(in.readString(MAX_TEXT));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every packet id this build knows, and what it decodes into.
|
||||
*
|
||||
* Written as plain data so that adding a packet is one line and an unknown
|
||||
* id is a loud error instead of a silent skip.
|
||||
*/
|
||||
public static Map<Integer, String> names() {
|
||||
Map<Integer, String> map = new LinkedHashMap<Integer, String>();
|
||||
map.put(Integer.valueOf(HELLO), "Hello");
|
||||
map.put(Integer.valueOf(HELLO_ACK), "HelloAck");
|
||||
map.put(Integer.valueOf(AUTH), "Auth");
|
||||
map.put(Integer.valueOf(AUTH_ACK), "AuthAck");
|
||||
map.put(Integer.valueOf(LOGIN), "Login");
|
||||
map.put(Integer.valueOf(LOGIN_ACK), "LoginAck");
|
||||
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
|
||||
return map;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user