Вход без пароля, права админа и F3+B
Аутентификация переделана по схеме пользователя: лаунчер — единственный
источник токена, сервер проверяет его сам и лаунчера не спрашивает.
- rsx-auth/GameToken: токен это base64url(payload) + "." + base64url(HMAC-SHA256),
payload {uuid, nick, aud, exp, jti, typ}, typ = session или reload.
- Сессия живёт час, reload-токен полтора часа и только внутри игры. Пакет
SessionReload продлевает сессию и выдаёт новый reload-токен.
- Сервер проверяет подпись, аудиенс, срок и совпадение uuid. HTTP-запрос к
лаунчеру из сервера убран.
- Клиент: окно пароля убрано совсем, ник берётся из токена, клиент сам
продлевает сессию за 5 минут до конца.
- Для локальной разработки сервер с --dev-token пишет свой токен в
world/dev-token.txt, клиент берёт токен оттуда.
- Права админа: свой список world/admins.txt, перечитывается раз в 5 секунд,
поэтому право снимается на лету. Пакет AdminState.
- Ванильные блоки мира ломает только админ, режим игры прав не даёт.
- F3+B только для админов: без прав показываем отказ, с правом включаются и
ванильные хитбоксы, и наш отладочный рендер. Права сняли посреди просмотра —
гаснут сразу.
- run-game.ps1 умеет -Token.
Проверено на живом сервере: own session ok, player in, rights of Player given.
Тесты: протокол 34, сервер 88, форк собирается чисто.
This commit is contained in:
@@ -100,7 +100,7 @@ public final class Packets {
|
||||
* is in. This is the part of the handshake that Yggdrasil does in
|
||||
* vanilla, done with our own launcher instead of Mojang's.
|
||||
*/
|
||||
public static final int LAUNCHER_AUTH = 0x1A;
|
||||
public static final int TOKEN_AUTH = 0x1A;
|
||||
|
||||
/**
|
||||
* The whole inventory, sent when the player arrives.
|
||||
@@ -170,6 +170,15 @@ public final class Packets {
|
||||
/** Who is in the world. */
|
||||
public static final int PLAYER_LIST = 0x30;
|
||||
|
||||
/** The client asks for a longer session. */
|
||||
public static final int SESSION_RELOAD = 0x31;
|
||||
|
||||
/** The session was made longer. */
|
||||
public static final int SESSION_RENEWED = 0x32;
|
||||
|
||||
/** Whether the player is one of the administrators. */
|
||||
public static final int ADMIN_STATE = 0x33;
|
||||
|
||||
/**
|
||||
* One slot of the inventory and what is in it now.
|
||||
*
|
||||
@@ -392,19 +401,39 @@ public final class Packets {
|
||||
public final String token;
|
||||
public final long expiresAt;
|
||||
|
||||
private AuthAck(boolean accepted, String reason, String token, long expiresAt) {
|
||||
/**
|
||||
* The token that makes the session longer when it runs out.
|
||||
*
|
||||
* It lives inside the game and for that: it does not touch the
|
||||
* account's own reload token, so nobody has to sign in to the
|
||||
* launcher again after every restart.
|
||||
*/
|
||||
public final String reloadToken;
|
||||
|
||||
/** When the reload token itself runs out. */
|
||||
public final long reloadExpiresAt;
|
||||
|
||||
private AuthAck(boolean accepted, String reason, String token, long expiresAt,
|
||||
String reloadToken, long reloadExpiresAt) {
|
||||
this.accepted = accepted;
|
||||
this.reason = reason;
|
||||
this.token = token;
|
||||
this.expiresAt = expiresAt;
|
||||
this.reloadToken = reloadToken;
|
||||
this.reloadExpiresAt = reloadExpiresAt;
|
||||
}
|
||||
|
||||
public static AuthAck ok(String token, long expiresAt) {
|
||||
return new AuthAck(true, "", token, expiresAt);
|
||||
return new AuthAck(true, "", token, expiresAt, "", 0L);
|
||||
}
|
||||
|
||||
public static AuthAck ok(String token, long expiresAt, String reloadToken,
|
||||
long reloadExpiresAt) {
|
||||
return new AuthAck(true, "", token, expiresAt, reloadToken, reloadExpiresAt);
|
||||
}
|
||||
|
||||
public static AuthAck refused(String reason) {
|
||||
return new AuthAck(false, reason, "", 0L);
|
||||
return new AuthAck(false, reason, "", 0L, "", 0L);
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
@@ -414,6 +443,8 @@ public final class Packets {
|
||||
if (this.accepted) {
|
||||
buf.writeString(this.token);
|
||||
buf.writeLong(this.expiresAt);
|
||||
buf.writeString(this.reloadToken);
|
||||
buf.writeLong(this.reloadExpiresAt);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
@@ -426,7 +457,8 @@ public final class Packets {
|
||||
if (!accepted) {
|
||||
return refused(reason);
|
||||
}
|
||||
return ok(in.readString(MAX_TEXT), in.readLong());
|
||||
return ok(in.readString(MAX_TEXT), in.readLong(), in.readString(MAX_TEXT),
|
||||
in.readLong());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,12 +1039,12 @@ public final class Packets {
|
||||
* sent to the game server and nowhere else, and the game server uses it
|
||||
* once, to ask the launcher whether it is good.
|
||||
*/
|
||||
public static final class LauncherAuth implements Packet {
|
||||
public static final class TokenAuth implements Packet {
|
||||
public final String account;
|
||||
public final String uuid;
|
||||
public final String token;
|
||||
|
||||
public LauncherAuth(String account, String uuid, String token) {
|
||||
public TokenAuth(String account, String uuid, String token) {
|
||||
this.account = account;
|
||||
this.uuid = uuid;
|
||||
this.token = token;
|
||||
@@ -1026,8 +1058,8 @@ public final class Packets {
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static LauncherAuth read(Buf in) {
|
||||
return new LauncherAuth(in.readString(MAX_TEXT), in.readString(MAX_TEXT),
|
||||
public static TokenAuth read(Buf in) {
|
||||
return new TokenAuth(in.readString(MAX_TEXT), in.readString(MAX_TEXT),
|
||||
in.readString(MAX_TEXT));
|
||||
}
|
||||
}
|
||||
@@ -1707,6 +1739,101 @@ public final class Packets {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The client asks for a longer session.
|
||||
*
|
||||
* The token in it is the one the server handed out with the session. It
|
||||
* lives inside the game and for that: it does not touch the account's own
|
||||
* reload token.
|
||||
*/
|
||||
public static final class SessionReload implements Packet {
|
||||
|
||||
public final String token;
|
||||
|
||||
public SessionReload(String token) {
|
||||
this.token = token == null ? "" : token;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf(64 + this.token.length());
|
||||
buf.writeString(this.token);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static SessionReload read(Buf in) {
|
||||
return new SessionReload(in.readString(MAX_TEXT));
|
||||
}
|
||||
}
|
||||
|
||||
/** The session was made longer. */
|
||||
public static final class SessionRenewed implements Packet {
|
||||
|
||||
public final boolean accepted;
|
||||
public final String reason;
|
||||
public final String token;
|
||||
public final long expiresAt;
|
||||
public final String reloadToken;
|
||||
public final long reloadExpiresAt;
|
||||
|
||||
public SessionRenewed(boolean accepted, String reason, String token, long expiresAt,
|
||||
String reloadToken, long reloadExpiresAt) {
|
||||
this.accepted = accepted;
|
||||
this.reason = reason == null ? "" : reason;
|
||||
this.token = token == null ? "" : token;
|
||||
this.expiresAt = expiresAt;
|
||||
this.reloadToken = reloadToken == null ? "" : reloadToken;
|
||||
this.reloadExpiresAt = reloadExpiresAt;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf(128);
|
||||
buf.writeBoolean(this.accepted);
|
||||
buf.writeString(this.reason);
|
||||
if (this.accepted) {
|
||||
buf.writeString(this.token);
|
||||
buf.writeLong(this.expiresAt);
|
||||
buf.writeString(this.reloadToken);
|
||||
buf.writeLong(this.reloadExpiresAt);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static SessionRenewed read(Buf in) {
|
||||
boolean accepted = in.readBoolean();
|
||||
String reason = in.readString(MAX_TEXT);
|
||||
if (!accepted) {
|
||||
return new SessionRenewed(false, reason, "", 0L, "", 0L);
|
||||
}
|
||||
return new SessionRenewed(true, reason, in.readString(MAX_TEXT), in.readLong(),
|
||||
in.readString(MAX_TEXT), in.readLong());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the player is one of the administrators.
|
||||
*
|
||||
* The server is the one who decides, and it can take the rights away
|
||||
* while the player is looking at the world.
|
||||
*/
|
||||
public static final class AdminState implements Packet {
|
||||
|
||||
public final boolean admin;
|
||||
|
||||
public AdminState(boolean admin) {
|
||||
this.admin = admin;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf(4);
|
||||
buf.writeBoolean(this.admin);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static AdminState read(Buf in) {
|
||||
return new AdminState(in.readBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every packet id this build knows, and what it decodes into.
|
||||
*
|
||||
@@ -1731,7 +1858,7 @@ public final class Packets {
|
||||
map.put(Integer.valueOf(PONG), "Pong");
|
||||
map.put(Integer.valueOf(ENTITY_SPAWN), "EntitySpawn");
|
||||
map.put(Integer.valueOf(ENTITY_REMOVE), "EntityRemove");
|
||||
map.put(Integer.valueOf(LAUNCHER_AUTH), "LauncherAuth");
|
||||
map.put(Integer.valueOf(TOKEN_AUTH), "TokenAuth");
|
||||
map.put(Integer.valueOf(SET_GAME_MODE), "GameMode");
|
||||
map.put(Integer.valueOf(INVENTORY), "Inventory");
|
||||
map.put(Integer.valueOf(INVENTORY_SLOT), "SlotChange");
|
||||
@@ -1754,6 +1881,9 @@ public final class Packets {
|
||||
map.put(Integer.valueOf(CHAT), "Chat");
|
||||
map.put(Integer.valueOf(CHAT_SEND), "ChatSend");
|
||||
map.put(Integer.valueOf(PLAYER_LIST), "PlayerList");
|
||||
map.put(Integer.valueOf(SESSION_RELOAD), "SessionReload");
|
||||
map.put(Integer.valueOf(SESSION_RENEWED), "SessionRenewed");
|
||||
map.put(Integer.valueOf(ADMIN_STATE), "AdminState");
|
||||
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -343,6 +343,33 @@ public final class ItemTest {
|
||||
eq("two names", Integer.valueOf(2), Integer.valueOf(listBack.names.length));
|
||||
eq("the first name", "One", listBack.names[0]);
|
||||
eq("the second mode", Integer.valueOf(1), Integer.valueOf(listBack.gameModes[1]));
|
||||
|
||||
Packets.AuthAck ack = Packets.AuthAck.ok("the session", 111L, "the reload", 222L);
|
||||
Packets.AuthAck ackBack = Packets.AuthAck.read(new Buf(ack.write().toByteArray()));
|
||||
yes("the session was made", ackBack.accepted);
|
||||
eq("the session token", "the session", ackBack.token);
|
||||
eq("when it runs out", Long.valueOf(111L), Long.valueOf(ackBack.expiresAt));
|
||||
eq("the reload token", "the reload", ackBack.reloadToken);
|
||||
eq("when the reload token runs out", Long.valueOf(222L),
|
||||
Long.valueOf(ackBack.reloadExpiresAt));
|
||||
|
||||
Packets.SessionReload reload = new Packets.SessionReload("the reload");
|
||||
Packets.SessionReload reloadBack =
|
||||
Packets.SessionReload.read(new Buf(reload.write().toByteArray()));
|
||||
eq("the reload token survives", "the reload", reloadBack.token);
|
||||
|
||||
Packets.SessionRenewed renewed = new Packets.SessionRenewed(true, "",
|
||||
"the session", 333L, "the reload", 444L);
|
||||
Packets.SessionRenewed renewedBack =
|
||||
Packets.SessionRenewed.read(new Buf(renewed.write().toByteArray()));
|
||||
yes("the session was made longer", renewedBack.accepted);
|
||||
eq("the new session token", "the session", renewedBack.token);
|
||||
eq("the new reload token", "the reload", renewedBack.reloadToken);
|
||||
|
||||
Packets.AdminState admin = new Packets.AdminState(true);
|
||||
Packets.AdminState adminBack =
|
||||
Packets.AdminState.read(new Buf(admin.write().toByteArray()));
|
||||
yes("the right is there", adminBack.admin);
|
||||
}
|
||||
|
||||
private static void near(Object what, double expected, double actual) {
|
||||
|
||||
@@ -488,10 +488,10 @@ public final class ProtocolTest {
|
||||
eq("remove", Integer.valueOf(11), Integer.valueOf(
|
||||
Packets.EntityRemove.read(new Buf(remove.write().toByteArray())).entityId));
|
||||
|
||||
Packets.LauncherAuth launcher = new Packets.LauncherAuth("Player",
|
||||
Packets.TokenAuth launcher = new Packets.TokenAuth("Player",
|
||||
"00000000-0000-0000-0000-000000000000", "a.b.c");
|
||||
Packets.LauncherAuth launcherBack =
|
||||
Packets.LauncherAuth.read(new Buf(launcher.write().toByteArray()));
|
||||
Packets.TokenAuth launcherBack =
|
||||
Packets.TokenAuth.read(new Buf(launcher.write().toByteArray()));
|
||||
eq("launcher account", "Player", launcherBack.account);
|
||||
eq("launcher uuid", "00000000-0000-0000-0000-000000000000", launcherBack.uuid);
|
||||
eq("launcher token", "a.b.c", launcherBack.token);
|
||||
@@ -544,7 +544,7 @@ public final class ProtocolTest {
|
||||
Packets.LOGIN, Packets.LOGIN_ACK, Packets.JOIN_WORLD,
|
||||
Packets.WORLD_JOIN, Packets.CHUNK_DATA, Packets.BLOCK_CHANGE,
|
||||
Packets.PLAYER_MOVE, Packets.ENTITY_MOVE, Packets.PING, Packets.PONG,
|
||||
Packets.ENTITY_SPAWN, Packets.ENTITY_REMOVE, Packets.LAUNCHER_AUTH,
|
||||
Packets.ENTITY_SPAWN, Packets.ENTITY_REMOVE, Packets.TOKEN_AUTH,
|
||||
Packets.SET_GAME_MODE, Packets.INVENTORY, Packets.INVENTORY_SLOT,
|
||||
Packets.DIG, Packets.BREAK_PROGRESS, Packets.CRAFT_START, Packets.CRAFT_UPDATE,
|
||||
Packets.RESEARCH_START, Packets.RESEARCH_UPDATE, Packets.TECH_TREE_UNLOCK,
|
||||
@@ -553,6 +553,7 @@ public final class ProtocolTest {
|
||||
Packets.LEARN_BLUEPRINT,
|
||||
Packets.HITBOXES, Packets.HITBOX_INSTANCES, Packets.HITBOXES_RELOAD,
|
||||
Packets.ATTACK, Packets.CHAT, Packets.CHAT_SEND, Packets.PLAYER_LIST,
|
||||
Packets.SESSION_RELOAD, Packets.SESSION_RENEWED, Packets.ADMIN_STATE,
|
||||
Packets.DISCONNECT,
|
||||
};
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
|
||||
Reference in New Issue
Block a user