Вход без пароля, права админа и 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user