mirror of https://github.com/Minestom/Minestom.git
217 lines
11 KiB
Java
217 lines
11 KiB
Java
package net.minestom.server.listener.preplay;
|
|
|
|
import com.google.gson.Gson;
|
|
import com.google.gson.JsonElement;
|
|
import com.google.gson.JsonObject;
|
|
import net.kyori.adventure.text.Component;
|
|
import net.kyori.adventure.text.format.NamedTextColor;
|
|
import net.minestom.server.MinecraftServer;
|
|
import net.minestom.server.entity.Player;
|
|
import net.minestom.server.extras.MojangAuth;
|
|
import net.minestom.server.extras.bungee.BungeeCordProxy;
|
|
import net.minestom.server.extras.mojangAuth.MojangCrypt;
|
|
import net.minestom.server.extras.velocity.VelocityProxy;
|
|
import net.minestom.server.network.ConnectionManager;
|
|
import net.minestom.server.network.NetworkBuffer;
|
|
import net.minestom.server.network.packet.client.login.ClientEncryptionResponsePacket;
|
|
import net.minestom.server.network.packet.client.login.ClientLoginAcknowledgedPacket;
|
|
import net.minestom.server.network.packet.client.login.ClientLoginPluginResponsePacket;
|
|
import net.minestom.server.network.packet.client.login.ClientLoginStartPacket;
|
|
import net.minestom.server.network.packet.server.login.EncryptionRequestPacket;
|
|
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
|
|
import net.minestom.server.network.player.GameProfile;
|
|
import net.minestom.server.network.player.PlayerConnection;
|
|
import net.minestom.server.network.player.PlayerSocketConnection;
|
|
import net.minestom.server.network.plugin.LoginPluginMessageProcessor;
|
|
import net.minestom.server.network.plugin.LoginPluginResponse;
|
|
import net.minestom.server.utils.async.AsyncUtils;
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
|
import javax.crypto.SecretKey;
|
|
import java.math.BigInteger;
|
|
import java.net.*;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.*;
|
|
import java.util.concurrent.ThreadLocalRandom;
|
|
|
|
import static net.minestom.server.network.NetworkBuffer.STRING;
|
|
|
|
public final class LoginListener {
|
|
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
|
|
private static final Gson GSON = new Gson();
|
|
|
|
private static final Component ALREADY_CONNECTED = Component.text("You are already on this server", NamedTextColor.RED);
|
|
private static final Component ERROR_DURING_LOGIN = Component.text("Error during login!", NamedTextColor.RED);
|
|
public static final Component INVALID_PROXY_RESPONSE = Component.text("Invalid proxy response!", NamedTextColor.RED);
|
|
|
|
public static void loginStartListener(@NotNull ClientLoginStartPacket packet, @NotNull PlayerConnection connection) {
|
|
final boolean isSocketConnection = connection instanceof PlayerSocketConnection;
|
|
// Proxy support (only for socket clients) and cache the login username
|
|
if (isSocketConnection) {
|
|
PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
|
|
socketConnection.UNSAFE_setLoginUsername(packet.username());
|
|
// Velocity support
|
|
if (VelocityProxy.isEnabled()) {
|
|
connection.loginPluginMessageProcessor().request(VelocityProxy.PLAYER_INFO_CHANNEL, null)
|
|
.thenAccept(response -> handleVelocityProxyResponse(socketConnection, response));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (MojangAuth.isEnabled() && isSocketConnection) {
|
|
// Mojang auth
|
|
if (CONNECTION_MANAGER.getOnlinePlayerByUsername(packet.username()) != null) {
|
|
connection.sendPacket(new LoginDisconnectPacket(ALREADY_CONNECTED));
|
|
connection.disconnect();
|
|
return;
|
|
}
|
|
final PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
|
|
|
|
final byte[] publicKey = MojangAuth.getKeyPair().getPublic().getEncoded();
|
|
byte[] nonce = new byte[4];
|
|
ThreadLocalRandom.current().nextBytes(nonce);
|
|
socketConnection.setNonce(nonce);
|
|
socketConnection.sendPacket(new EncryptionRequestPacket("", publicKey, nonce));
|
|
} else {
|
|
final boolean bungee = BungeeCordProxy.isEnabled();
|
|
// Offline
|
|
final UUID playerUuid = bungee && isSocketConnection ?
|
|
((PlayerSocketConnection) connection).gameProfile().uuid() :
|
|
CONNECTION_MANAGER.getPlayerConnectionUuid(connection, packet.username());
|
|
CONNECTION_MANAGER.createPlayer(connection, playerUuid, packet.username());
|
|
}
|
|
}
|
|
|
|
public static void loginEncryptionResponseListener(@NotNull ClientEncryptionResponsePacket packet, @NotNull PlayerConnection connection) {
|
|
// Encryption is only support for socket connection
|
|
if (!(connection instanceof PlayerSocketConnection socketConnection)) return;
|
|
AsyncUtils.runAsync(() -> {
|
|
final String loginUsername = socketConnection.getLoginUsername();
|
|
if (loginUsername == null || loginUsername.isEmpty()) {
|
|
// Shouldn't happen
|
|
return;
|
|
}
|
|
|
|
final boolean hasPublicKey = connection.playerPublicKey() != null;
|
|
final boolean verificationFailed = hasPublicKey || !Arrays.equals(socketConnection.getNonce(),
|
|
MojangCrypt.decryptUsingKey(MojangAuth.getKeyPair().getPrivate(), packet.encryptedVerifyToken()));
|
|
|
|
if (verificationFailed) {
|
|
MinecraftServer.LOGGER.error("Encryption failed for {}", loginUsername);
|
|
return;
|
|
}
|
|
|
|
final byte[] digestedData = MojangCrypt.digestData("", MojangAuth.getKeyPair().getPublic(), getSecretKey(packet.sharedSecret()));
|
|
if (digestedData == null) {
|
|
// Incorrect key, probably because of the client
|
|
MinecraftServer.LOGGER.error("Connection {} failed initializing encryption.", socketConnection.getRemoteAddress());
|
|
connection.disconnect();
|
|
return;
|
|
}
|
|
// Query Mojang's session server.
|
|
final String serverId = new BigInteger(digestedData).toString(16);
|
|
final String username = URLEncoder.encode(loginUsername, StandardCharsets.UTF_8);
|
|
|
|
final String url = String.format(MojangAuth.AUTH_URL, username, serverId);
|
|
// TODO: Add ability to add ip query tag. See: https://wiki.vg/Protocol_Encryption#Authentication
|
|
|
|
final HttpClient client = HttpClient.newHttpClient();
|
|
final HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
|
|
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).whenComplete((response, throwable) -> {
|
|
final boolean ok = throwable == null && response.statusCode() == 200 && response.body() != null && !response.body().isEmpty();
|
|
|
|
if (!ok) {
|
|
if (throwable != null) {
|
|
MinecraftServer.getExceptionManager().handleException(throwable);
|
|
}
|
|
|
|
if (socketConnection.getPlayer() != null) {
|
|
socketConnection.getPlayer().kick(Component.text("Failed to contact Mojang's Session Servers (Are they down?)"));
|
|
} else {
|
|
socketConnection.disconnect();
|
|
}
|
|
return;
|
|
}
|
|
try {
|
|
final JsonObject gameProfile = GSON.fromJson(response.body(), JsonObject.class);
|
|
socketConnection.setEncryptionKey(getSecretKey(packet.sharedSecret()));
|
|
UUID profileUUID = java.util.UUID.fromString(gameProfile.get("id").getAsString()
|
|
.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"));
|
|
final String profileName = gameProfile.get("name").getAsString();
|
|
|
|
MinecraftServer.LOGGER.info("UUID of player {} is {}", loginUsername, profileUUID);
|
|
CONNECTION_MANAGER.createPlayer(connection, profileUUID, profileName);
|
|
List<GameProfile.Property> propertyList = new ArrayList<>();
|
|
for (JsonElement element : gameProfile.get("properties").getAsJsonArray()) {
|
|
JsonObject object = element.getAsJsonObject();
|
|
propertyList.add(new GameProfile.Property(object.get("name").getAsString(), object.get("value").getAsString(), object.get("signature").getAsString()));
|
|
}
|
|
socketConnection.UNSAFE_setProfile(new GameProfile(profileUUID, profileName, propertyList));
|
|
} catch (Exception e) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private static SecretKey getSecretKey(byte[] sharedSecret) {
|
|
return MojangCrypt.decryptByteToSecretKey(MojangAuth.getKeyPair().getPrivate(), sharedSecret);
|
|
}
|
|
|
|
private static void handleVelocityProxyResponse(PlayerSocketConnection socketConnection, LoginPluginResponse response) {
|
|
byte[] data = response.getPayload();
|
|
|
|
SocketAddress socketAddress = null;
|
|
GameProfile gameProfile = null;
|
|
boolean success = false;
|
|
if (data != null && data.length > 0) {
|
|
NetworkBuffer buffer = new NetworkBuffer(ByteBuffer.wrap(data));
|
|
success = VelocityProxy.checkIntegrity(buffer);
|
|
if (success) {
|
|
// Get the real connection address
|
|
final InetAddress address;
|
|
try {
|
|
address = InetAddress.getByName(buffer.read(STRING));
|
|
} catch (UnknownHostException e) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
return;
|
|
}
|
|
final int port = ((java.net.InetSocketAddress) socketConnection.getRemoteAddress()).getPort();
|
|
socketAddress = new InetSocketAddress(address, port);
|
|
gameProfile = new GameProfile(buffer);
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
socketConnection.setRemoteAddress(socketAddress);
|
|
socketConnection.UNSAFE_setProfile(gameProfile);
|
|
CONNECTION_MANAGER.createPlayer(socketConnection, gameProfile.uuid(), gameProfile.name());
|
|
} else {
|
|
LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE);
|
|
socketConnection.sendPacket(disconnectPacket);
|
|
}
|
|
}
|
|
|
|
public static void loginPluginResponseListener(@NotNull ClientLoginPluginResponsePacket packet, @NotNull PlayerConnection connection) {
|
|
try {
|
|
LoginPluginMessageProcessor messageProcessor = connection.loginPluginMessageProcessor();
|
|
messageProcessor.handleResponse(packet.messageId(), packet.data());
|
|
} catch (Throwable t) {
|
|
MinecraftServer.LOGGER.error("Error handling Login Plugin Response", t);
|
|
LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(ERROR_DURING_LOGIN);
|
|
connection.sendPacket(disconnectPacket);
|
|
connection.disconnect();
|
|
}
|
|
}
|
|
|
|
public static void loginAckListener(@NotNull ClientLoginAcknowledgedPacket ignored, @NotNull PlayerConnection connection) {
|
|
final Player player = Objects.requireNonNull(connection.getPlayer());
|
|
CONNECTION_MANAGER.doConfiguration(player, true);
|
|
}
|
|
|
|
}
|