Вход без пароля, права админа и 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:
@@ -1302,3 +1302,28 @@ import org.lwjgl.opengl.ContextCapabilities;
|
||||
net.rustnotmc.client.net.RsxNetworkManager.sendChat(s);
|
||||
}
|
||||
|
||||
# F3+B shows the boxes of the world, and that belongs to the administrators.
|
||||
# The server says who they are, and a right that is taken away while the
|
||||
# player is looking takes the boxes with it.
|
||||
=== FILE net/minecraft/client/Minecraft.java
|
||||
--- BEFORE
|
||||
if (i == 48) {
|
||||
flag = !this.renderManager.isDebugBoundingBox();
|
||||
this.renderManager.setDebugBoundingBox(flag);
|
||||
this.debugFeedbackTranslated(flag ? "debug.show_hitboxes.on" : "debug.show_hitboxes.off");
|
||||
return true;
|
||||
--- AFTER
|
||||
if (i == 48) {
|
||||
if (!net.rustnotmc.client.net.RsxClientMode.isAdmin()) {
|
||||
this.ingameGUI.getChatGUI().printChatMessage(
|
||||
new net.minecraft.util.text.TextComponentString(
|
||||
"only an administrator may look at the boxes"));
|
||||
return true;
|
||||
}
|
||||
|
||||
flag = !this.renderManager.isDebugBoundingBox();
|
||||
this.renderManager.setDebugBoundingBox(flag);
|
||||
net.rustnotmc.client.RsxHitboxDebug.setVisible(flag);
|
||||
this.debugFeedbackTranslated(flag ? "debug.show_hitboxes.on" : "debug.show_hitboxes.off");
|
||||
return true;
|
||||
|
||||
|
||||
@@ -6,55 +6,43 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.gui.GuiDisconnected;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.gui.GuiTextField;
|
||||
import net.minecraft.util.text.TextComponentTranslation;
|
||||
import net.rustnotmc.client.gui.RsxPasswordField;
|
||||
import net.rustnotmc.auth.GameToken;
|
||||
import net.rustnotmc.client.net.RsxNetworkManager;
|
||||
import net.rustnotmc.client.net.RsxWorldBridge;
|
||||
|
||||
/**
|
||||
* The screen that gets the player into a server.
|
||||
*
|
||||
* The launcher is where accounts live, so the launcher signs the player in and
|
||||
* hands the game a token. This screen then has nothing to ask: it takes the
|
||||
* token out of the session the launcher built and offers it to the server.
|
||||
* There is nothing to fill in here. The launcher asked the auth server
|
||||
* whether the player may play, and when the answer was yes it gave the game a
|
||||
* token and a uuid. The game shows both to the server, the server checks them
|
||||
* itself, and the player is in.
|
||||
*
|
||||
* When the game was started without a launcher there is no session and
|
||||
* somebody has to say who they are, so the screen asks for a name and a
|
||||
* password. That is the way in for a server of our own and the way in for
|
||||
* development; a server with a launcher in front of it does not accept it.
|
||||
*
|
||||
* This replaces the game's own connecting screen, which is where the player
|
||||
* already is when they press "join server", so the flow stays the one they
|
||||
* know.
|
||||
* So this screen has no account field and no password field: a password is
|
||||
* something the launcher knows, not something the game asks for. All it does
|
||||
* is open the socket and say what is happening.
|
||||
*/
|
||||
public class GuiConnecting extends GuiScreen {
|
||||
|
||||
/** What the launcher leaves in the session when nobody signed in. */
|
||||
private static final String NO_TOKEN = "0";
|
||||
|
||||
private final GuiScreen previousGuiScreen;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final GuiScreen previousGuiScreen;
|
||||
|
||||
private GuiTextField accountField;
|
||||
private RsxPasswordField passwordField;
|
||||
private RsxNetworkManager networkManager;
|
||||
private volatile String status = "";
|
||||
|
||||
/** The launcher's session, if the game was started by the launcher. */
|
||||
private String launcherAccount;
|
||||
private String launcherUuid;
|
||||
private String launcherToken;
|
||||
private String launcherAccount = "";
|
||||
private String launcherUuid = "";
|
||||
private String launcherToken = "";
|
||||
private String status = "";
|
||||
|
||||
public GuiConnecting(GuiScreen guiscreen, Minecraft minecraft, ServerData serverdata) {
|
||||
this.mc = minecraft;
|
||||
this.previousGuiScreen = guiscreen;
|
||||
ServerAddress serveraddress = ServerAddress.fromString(serverdata.serverIP);
|
||||
minecraft.loadWorld((WorldClient) null);
|
||||
minecraft.setServerData(serverdata);
|
||||
this.host = serveraddress.getIP();
|
||||
this.port = serveraddress.getPort();
|
||||
this(guiscreen, minecraft, ServerAddress.fromString(serverdata.serverIP));
|
||||
}
|
||||
|
||||
public GuiConnecting(GuiScreen guiscreen, Minecraft minecraft, ServerAddress address) {
|
||||
this(guiscreen, minecraft, address.getIP(), address.getPort());
|
||||
}
|
||||
|
||||
public GuiConnecting(GuiScreen guiscreen, Minecraft minecraft, String s, int i) {
|
||||
@@ -68,34 +56,18 @@ public class GuiConnecting extends GuiScreen {
|
||||
public void initGui() {
|
||||
this.buttonList.clear();
|
||||
|
||||
// The launcher built the session: the name, the identity and the token.
|
||||
this.launcherAccount = sessionValue(this.mc, 0);
|
||||
this.launcherUuid = sessionValue(this.mc, 1);
|
||||
this.launcherToken = sessionValue(this.mc, 2);
|
||||
|
||||
this.accountField = new GuiTextField(0, this.fontRenderer, this.width / 2 - 100,
|
||||
this.height / 4 + 20, 200, 20);
|
||||
this.accountField.setMaxStringLength(32);
|
||||
this.accountField.setText(this.launcherAccount == null
|
||||
? this.mc.getSession().getProfile().getName() : this.launcherAccount);
|
||||
|
||||
this.passwordField = new RsxPasswordField(1, this.fontRenderer, this.width / 2 - 100,
|
||||
this.height / 4 + 50, 200, 20);
|
||||
this.passwordField.setMaxStringLength(128);
|
||||
|
||||
// two buttons side by side, and they must not lie on top of each other:
|
||||
// the plain constructor makes a button two hundred wide
|
||||
this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 4 + 90,
|
||||
98, 20, "Connect"));
|
||||
this.buttonList.add(new GuiButton(1, this.width / 2 + 2, this.height / 4 + 90,
|
||||
98, 20, "Cancel"));
|
||||
this.buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 4 + 90,
|
||||
200, 20, "Cancel"));
|
||||
|
||||
if (this.hasLauncherSession()) {
|
||||
// the launcher already signed the player in, so there is nothing to
|
||||
// ask for and nothing to wait for
|
||||
this.passwordField.setFocused(true);
|
||||
this.start();
|
||||
} else {
|
||||
this.passwordField.setFocused(true);
|
||||
this.status = "the launcher did not give a session";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,32 +87,8 @@ public class GuiConnecting extends GuiScreen {
|
||||
reason == null ? "the connection was closed" : reason})));
|
||||
}
|
||||
|
||||
protected void keyTyped(char c0, int i) {
|
||||
if (c0 == '\r' || c0 == '\n') {
|
||||
this.start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.accountField.isFocused()) {
|
||||
this.accountField.textboxKeyTyped(c0, i);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.passwordField.isFocused()) {
|
||||
this.passwordField.textboxKeyTyped(c0, i);
|
||||
}
|
||||
}
|
||||
|
||||
public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
|
||||
super.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
this.accountField.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
this.passwordField.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
}
|
||||
|
||||
protected void actionPerformed(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
this.start();
|
||||
} else if (guibutton.id == 1) {
|
||||
if (guibutton.id == 1) {
|
||||
this.mc.displayGuiScreen(this.previousGuiScreen);
|
||||
}
|
||||
}
|
||||
@@ -151,15 +99,8 @@ public class GuiConnecting extends GuiScreen {
|
||||
this.height / 4 - 20, 16777215);
|
||||
|
||||
if (this.hasLauncherSession()) {
|
||||
this.drawCenteredString(this.fontRenderer, "signed in as " + this.launcherAccount,
|
||||
this.drawCenteredString(this.fontRenderer, "signed in as " + account(),
|
||||
this.width / 2, this.height / 4 + 10, 8421504);
|
||||
} else {
|
||||
this.drawString(this.fontRenderer, "Account", this.width / 2 - 100,
|
||||
this.height / 4 + 10, 8421504);
|
||||
this.accountField.drawTextBox();
|
||||
this.drawString(this.fontRenderer, "Password", this.width / 2 - 100,
|
||||
this.height / 4 + 40, 8421504);
|
||||
this.passwordField.drawTextBox();
|
||||
}
|
||||
|
||||
if (!this.status.isEmpty()) {
|
||||
@@ -170,6 +111,20 @@ public class GuiConnecting extends GuiScreen {
|
||||
super.drawScreen(i, j, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the player.
|
||||
*
|
||||
* It is inside the token: the launcher gives the game a token and a uuid
|
||||
* without a name, and the name is read out of the token.
|
||||
*/
|
||||
private String account() {
|
||||
java.util.Map<String, String> claims = GameToken.peek(this.launcherToken);
|
||||
if (claims != null && claims.get("nick") != null && !claims.get("nick").isEmpty()) {
|
||||
return claims.get("nick");
|
||||
}
|
||||
return this.launcherAccount == null ? "" : this.launcherAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the connection.
|
||||
*
|
||||
@@ -180,36 +135,22 @@ public class GuiConnecting extends GuiScreen {
|
||||
if (this.networkManager != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String account;
|
||||
String token = null;
|
||||
String uuid = null;
|
||||
char[] password = new char[0];
|
||||
|
||||
if (this.hasLauncherSession()) {
|
||||
account = this.launcherAccount;
|
||||
token = this.launcherToken;
|
||||
uuid = this.launcherUuid;
|
||||
} else {
|
||||
account = this.accountField.getText().trim();
|
||||
password = this.passwordField.getText().toCharArray();
|
||||
if (account.isEmpty()) {
|
||||
this.status = "an account name is needed";
|
||||
return;
|
||||
}
|
||||
if (password.length == 0) {
|
||||
this.status = "a password is needed";
|
||||
return;
|
||||
}
|
||||
this.passwordField.setText("");
|
||||
if (!this.hasLauncherSession()) {
|
||||
this.status = "the launcher did not give a session";
|
||||
return;
|
||||
}
|
||||
|
||||
String account = account();
|
||||
String token = this.launcherToken;
|
||||
String uuid = this.launcherUuid;
|
||||
|
||||
this.status = "connecting to " + this.host + ":" + this.port;
|
||||
System.out.println("[rsx] connecting to " + this.host + ":" + this.port + " as " + account);
|
||||
System.out.println("[rsx] connecting to " + this.host + ":" + this.port
|
||||
+ " as " + account);
|
||||
|
||||
final RsxNetworkManager manager = new RsxNetworkManager(this.mc, this.previousGuiScreen,
|
||||
new GameProfile(RsxWorldBridge.uuidFor(account), account),
|
||||
account, password, token, uuid);
|
||||
account, new char[0], token, uuid);
|
||||
this.networkManager = manager;
|
||||
|
||||
(new Thread("Rsx Connect") {
|
||||
|
||||
@@ -36,6 +36,14 @@ public final class RsxBootstrap {
|
||||
private static boolean frameFailed;
|
||||
private static boolean tickFailed;
|
||||
|
||||
/**
|
||||
* How long before the session runs out the client makes it longer.
|
||||
*
|
||||
* Five minutes is enough to reach the server even when the connection is
|
||||
* not very good.
|
||||
*/
|
||||
private static final long RELOAD_MARGIN_MILLIS = 5L * 60L * 1000L;
|
||||
|
||||
private RsxBootstrap() {
|
||||
}
|
||||
|
||||
@@ -182,9 +190,20 @@ public final class RsxBootstrap {
|
||||
public static synchronized void clientTick() {
|
||||
// the connection is pumped from here, not from the screen that started
|
||||
// it. A handshake that only runs while a screen is up is a handshake
|
||||
// that the player can break by moving the window.
|
||||
// the player can break by moving the window.
|
||||
net.rustnotmc.client.net.RsxNetworkManager.pump();
|
||||
|
||||
// the session runs out after an hour, so it is made longer before that
|
||||
net.rustnotmc.client.net.RsxNetworkManager manager =
|
||||
net.rustnotmc.client.net.RsxNetworkManager.active();
|
||||
if (manager != null) {
|
||||
long left = manager.sessionLeft();
|
||||
if (left > 0L && left < RELOAD_MARGIN_MILLIS) {
|
||||
manager.reloadSession();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (loader == null || broken || tickFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,19 @@ public final class RsxHitboxDebug {
|
||||
return visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the boxes.
|
||||
*
|
||||
* The right to look at them belongs to the administrators, so when the
|
||||
* right is taken away the boxes go at once.
|
||||
*/
|
||||
public static void setVisible(boolean value) {
|
||||
visible = value;
|
||||
if (!value) {
|
||||
preview = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** How many models the server has sent. */
|
||||
public static int modelCount() {
|
||||
return MODELS.size();
|
||||
|
||||
@@ -21,6 +21,9 @@ public final class RsxClientMode {
|
||||
|
||||
private static volatile int mode = Packets.GAMEMODE_SURVIVAL;
|
||||
|
||||
/** Whether the server says the player is one of the administrators. */
|
||||
private static volatile boolean admin;
|
||||
|
||||
private RsxClientMode() {
|
||||
}
|
||||
|
||||
@@ -34,6 +37,35 @@ public final class RsxClientMode {
|
||||
mode = newMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the player is one of the administrators.
|
||||
*
|
||||
* The server is the one who decides, and it can take the right away while
|
||||
* the player is looking at the world. The things that only an
|
||||
* administrator may do are not offered to anybody else.
|
||||
*/
|
||||
public static boolean isAdmin() {
|
||||
return admin;
|
||||
}
|
||||
|
||||
/** The server said whether the player is an administrator. */
|
||||
public static void setAdmin(boolean value) {
|
||||
if (admin == value) {
|
||||
return;
|
||||
}
|
||||
admin = value;
|
||||
if (value) {
|
||||
return;
|
||||
}
|
||||
// The right was taken away, so the things that needed it go at once: the
|
||||
// boxes of the world and our own debug view with them.
|
||||
Minecraft minecraft = Minecraft.getMinecraft();
|
||||
if (minecraft.getRenderManager() != null) {
|
||||
minecraft.getRenderManager().setDebugBoundingBox(false);
|
||||
}
|
||||
net.rustnotmc.client.RsxHitboxDebug.setVisible(false);
|
||||
}
|
||||
|
||||
/** True when the player may break blocks. */
|
||||
public static boolean canBreakBlocks() {
|
||||
return mode == Packets.GAMEMODE_CREATIVE;
|
||||
|
||||
@@ -77,6 +77,12 @@ public final class RsxNetworkManager extends NetworkManager {
|
||||
|
||||
/** The names of the players the server told us about last time. */
|
||||
private java.util.List<String> knownPlayers = new java.util.ArrayList<String>();
|
||||
|
||||
/** When the session runs out. */
|
||||
private volatile long sessionExpiresAt;
|
||||
|
||||
/** The token that makes the session longer when it runs out. */
|
||||
private volatile String reloadToken = "";
|
||||
private final FrameDecoder decoder = new FrameDecoder();
|
||||
|
||||
private volatile Socket socket;
|
||||
@@ -276,6 +282,20 @@ public final class RsxNetworkManager extends NetworkManager {
|
||||
this.updatePlayerList(list);
|
||||
return;
|
||||
}
|
||||
case Packets.ADMIN_STATE: {
|
||||
Packets.AdminState state = Packets.AdminState.read(new Buf(frame.body()));
|
||||
RsxClientMode.setAdmin(state.admin);
|
||||
return;
|
||||
}
|
||||
case Packets.SESSION_RENEWED: {
|
||||
Packets.SessionRenewed renewed =
|
||||
Packets.SessionRenewed.read(new Buf(frame.body()));
|
||||
if (renewed.accepted) {
|
||||
this.sessionExpiresAt = renewed.expiresAt;
|
||||
this.reloadToken = renewed.reloadToken;
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
onPlay(frame);
|
||||
}
|
||||
@@ -378,14 +398,14 @@ public final class RsxNetworkManager extends NetworkManager {
|
||||
if (this.token != null && !this.token.isEmpty()) {
|
||||
// the launcher already signed the player in, and the token it
|
||||
// handed the game is all the game server needs
|
||||
send(Packets.LAUNCHER_AUTH,
|
||||
new Packets.LauncherAuth(this.account, this.uuid, this.token));
|
||||
send(Packets.TOKEN_AUTH,
|
||||
new Packets.TokenAuth(this.account, this.uuid, this.token));
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] proof = Packets.Auth.proofFor(this.account, this.password, this.salt, this.nonce);
|
||||
java.util.Arrays.fill(this.password, '\0');
|
||||
send(Packets.AUTH, new Packets.Auth(this.account, proof));
|
||||
// without a session there is nothing to offer, and the game never
|
||||
// asks the player for a password
|
||||
lost("the launcher did not give a session");
|
||||
}
|
||||
|
||||
private void onAuthAck(Frame frame) {
|
||||
@@ -394,9 +414,33 @@ public final class RsxNetworkManager extends NetworkManager {
|
||||
lost(auth.reason);
|
||||
return;
|
||||
}
|
||||
// the session and the token that makes it longer are ours now
|
||||
this.sessionExpiresAt = auth.expiresAt;
|
||||
this.reloadToken = auth.reloadToken;
|
||||
send(Packets.LOGIN, new Packets.Login(this.account, auth.token));
|
||||
}
|
||||
|
||||
/**
|
||||
* The session runs out in a while, so it is made longer before that.
|
||||
*
|
||||
* The token in it lives inside the game and for that: it has nothing to
|
||||
* do with the reload token of the account in the launcher.
|
||||
*/
|
||||
public void reloadSession() {
|
||||
if (this.reloadToken == null || this.reloadToken.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
send(Packets.SESSION_RELOAD, new Packets.SessionReload(this.reloadToken));
|
||||
}
|
||||
|
||||
/** How long is left of the session, in milliseconds. */
|
||||
public long sessionLeft() {
|
||||
if (this.sessionExpiresAt <= 0L) {
|
||||
return 0L;
|
||||
}
|
||||
return this.sessionExpiresAt - System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void onLoginAck(Frame frame) {
|
||||
Packets.LoginAck login = Packets.LoginAck.read(new Buf(frame.body()));
|
||||
if (!login.accepted) {
|
||||
@@ -520,6 +564,7 @@ public final class RsxNetworkManager extends NetworkManager {
|
||||
|
||||
private void lost(String reason) {
|
||||
this.failure = reason == null ? "the connection was lost" : reason;
|
||||
System.out.println("[rsx] connection lost: " + this.failure);
|
||||
this.open = false;
|
||||
Socket socket = this.socket;
|
||||
if (socket != null) {
|
||||
|
||||
Reference in New Issue
Block a user