Moved skin uploading to the global api

This commit is contained in:
Tim203 2021-02-12 22:22:45 +01:00
parent cf149b58e0
commit 52ddf8c556
No known key found for this signature in database
GPG key ID: 064EE9F5BF7C3EE8
12 changed files with 363 additions and 200 deletions

View file

@ -37,7 +37,7 @@ import lombok.Getter;
@Getter @Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class BedrockData implements Cloneable { public final class BedrockData implements Cloneable {
public static final int EXPECTED_LENGTH = 10; public static final int EXPECTED_LENGTH = 12;
private final String version; private final String version;
private final String username; private final String username;
@ -50,19 +50,24 @@ public final class BedrockData implements Cloneable {
private final LinkedPlayer linkedPlayer; private final LinkedPlayer linkedPlayer;
private final boolean fromProxy; private final boolean fromProxy;
private final int subscribeId;
private final String verifyCode;
private final int dataLength; private final int dataLength;
public static BedrockData of(String version, String username, String xuid, int deviceOs, public static BedrockData of(String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip, String languageCode, int uiProfile, int inputMode, String ip,
LinkedPlayer linkedPlayer, boolean fromProxy) { LinkedPlayer linkedPlayer, boolean fromProxy, int subscribeId,
String verifyCode) {
return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode, return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode,
uiProfile, ip, linkedPlayer, fromProxy, EXPECTED_LENGTH); uiProfile, ip, linkedPlayer, fromProxy, subscribeId, verifyCode, EXPECTED_LENGTH);
} }
public static BedrockData of(String version, String username, String xuid, int deviceOs, public static BedrockData of(String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip) { String languageCode, int uiProfile, int inputMode, String ip,
return of(version, username, xuid, deviceOs, languageCode, int subscribeId, String verifyCode) {
uiProfile, inputMode, ip, null, false); return of(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null,
false, subscribeId, verifyCode);
} }
public static BedrockData fromString(String data) { public static BedrockData fromString(String data) {
@ -75,13 +80,14 @@ public final class BedrockData implements Cloneable {
// The format is the same as the order of the fields in this class // The format is the same as the order of the fields in this class
return new BedrockData( return new BedrockData(
split[0], split[1], split[2], Integer.parseInt(split[3]), split[4], split[0], split[1], split[2], Integer.parseInt(split[3]), split[4],
Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], linkedPlayer,
linkedPlayer, "1".equals(split[8]), split.length "1".equals(split[9]), Integer.parseInt(split[10]), split[11], split.length
); );
} }
private static BedrockData emptyData(int dataLength) { private static BedrockData emptyData(int dataLength) {
return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, dataLength); return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, -1, null,
dataLength);
} }
public boolean hasPlayerLink() { public boolean hasPlayerLink() {
@ -94,7 +100,8 @@ public final class BedrockData implements Cloneable {
return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' + return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' +
languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' + languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' +
(fromProxy ? 1 : 0) + '\0' + (fromProxy ? 1 : 0) + '\0' +
(linkedPlayer != null ? linkedPlayer.toString() : "null"); (linkedPlayer != null ? linkedPlayer.toString() : "null") + '\0' +
subscribeId + '\0' + verifyCode;
} }
@Override @Override

View file

@ -1,106 +0,0 @@
/*
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.floodgate.util;
import lombok.AllArgsConstructor;
import java.nio.ByteBuffer;
import java.util.Base64;
import static java.lang.String.format;
@AllArgsConstructor
public final class RawSkin {
public int width;
public int height;
public byte[] data;
public boolean alex;
private RawSkin() {
}
public static RawSkin decode(byte[] data, int offset) throws InvalidFormatException {
if (data == null || offset < 0 || data.length <= offset) {
return null;
}
if (offset == 0) {
return decode(data);
}
byte[] rawSkin = new byte[data.length - offset];
System.arraycopy(data, offset, rawSkin, 0, rawSkin.length);
return decode(rawSkin);
}
public static RawSkin decode(byte[] data) throws InvalidFormatException {
// offset is an amount of bytes before the Base64 starts
if (data == null) {
return null;
}
int maxEncodedLength = Base64Utils.getEncodedLength(64 * 64 * 4 + 9);
// if the RawSkin is longer then the max Java Edition skin length
if (data.length > maxEncodedLength) {
throw new InvalidFormatException(format(
"Encoded data cannot be longer then %s bytes! Got %s",
maxEncodedLength, data.length
));
}
// if the encoded data doesn't even contain the width, height (8 bytes, 2 ints) and isAlex
if (data.length < Base64Utils.getEncodedLength(9)) {
throw new InvalidFormatException("Encoded data must be at least 16 bytes long!");
}
data = Base64.getDecoder().decode(data);
ByteBuffer buffer = ByteBuffer.wrap(data);
RawSkin skin = new RawSkin();
skin.width = buffer.getInt();
skin.height = buffer.getInt();
if (buffer.remaining() - 1 != (skin.width * skin.height * 4)) {
throw new InvalidFormatException(format(
"Expected skin length to be %s, got %s",
(skin.width * skin.height * 4), buffer.remaining()
));
}
skin.data = new byte[buffer.remaining() - 1];
buffer.get(skin.data);
skin.alex = buffer.get() == 1;
return skin;
}
public byte[] encode() {
// 2 x int + 1 = 9 bytes
ByteBuffer buffer = ByteBuffer.allocate(9 + data.length);
buffer.putInt(width);
buffer.putInt(height);
buffer.put(data);
buffer.put((byte) (alex ? 1 : 0));
return Base64.getEncoder().encode(buffer.array());
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.floodgate.util;
public enum WebsocketEventType {
SUBSCRIBER_CREATED,
SUBSCRIBERS_COUNT,
ADDED_TO_QUEUE,
SKIN_UPLOADED,
CREATOR_DISCONNECTED;
public static final WebsocketEventType[] VALUES = values();
public static WebsocketEventType getById(int id) {
return VALUES.length > id ? VALUES[id] : null;
}
}

View file

@ -22,6 +22,11 @@
<version>2.9.8</version> <version>2.9.8</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-Websocket</artifactId>
<version>1.5.1</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>

View file

@ -56,6 +56,7 @@ import org.geysermc.connector.network.translators.world.WorldManager;
import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.network.translators.world.block.BlockTranslator;
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
import org.geysermc.connector.skin.FloodgateSkinUploader;
import org.geysermc.connector.utils.DimensionUtils; import org.geysermc.connector.utils.DimensionUtils;
import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.connector.utils.LocaleUtils; import org.geysermc.connector.utils.LocaleUtils;
@ -108,6 +109,7 @@ public class GeyserConnector {
private AuthType authType; private AuthType authType;
private FloodgateCipher cipher; private FloodgateCipher cipher;
private FloodgateSkinUploader skinUploader;
private boolean shuttingDown = false; private boolean shuttingDown = false;
@ -203,6 +205,7 @@ public class GeyserConnector {
cipher = new AesCipher(new Base64Topping()); cipher = new AesCipher(new Base64Topping());
cipher.init(key); cipher.init(key);
logger.info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); logger.info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
skinUploader = new FloodgateSkinUploader(this).start();
} catch (Exception exception) { } catch (Exception exception) {
logger.severe(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), exception); logger.severe(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), exception);
} }

View file

@ -67,10 +67,10 @@ import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.entity.Tickable;
import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.CommandSender;
import org.geysermc.connector.common.AuthType; import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.Entity;
import org.geysermc.connector.entity.Tickable;
import org.geysermc.connector.entity.player.SessionPlayerEntity; import org.geysermc.connector.entity.player.SessionPlayerEntity;
import org.geysermc.connector.entity.player.SkullPlayerEntity; import org.geysermc.connector.entity.player.SkullPlayerEntity;
import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.inventory.PlayerInventory;
@ -85,6 +85,7 @@ import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.network.translators.collision.CollisionManager; import org.geysermc.connector.network.translators.collision.CollisionManager;
import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator; import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator;
import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.skin.FloodgateSkinUploader;
import org.geysermc.connector.skin.SkinManager; import org.geysermc.connector.skin.SkinManager;
import org.geysermc.connector.utils.*; import org.geysermc.connector.utils.*;
import org.geysermc.cumulus.Form; import org.geysermc.cumulus.Form;
@ -553,7 +554,9 @@ public class GeyserSession implements CommandSender {
byte[] encryptedData; byte[] encryptedData;
try { try {
FloodgateSkinUploader skinUploader = connector.getSkinUploader();
FloodgateCipher cipher = connector.getCipher(); FloodgateCipher cipher = connector.getCipher();
encryptedData = cipher.encryptFromString(BedrockData.of( encryptedData = cipher.encryptFromString(BedrockData.of(
clientData.getGameVersion(), clientData.getGameVersion(),
authData.getName(), authData.getName(),
@ -562,7 +565,9 @@ public class GeyserSession implements CommandSender {
clientData.getLanguageCode(), clientData.getLanguageCode(),
clientData.getUiProfile().ordinal(), clientData.getUiProfile().ordinal(),
clientData.getCurrentInputMode().ordinal(), clientData.getCurrentInputMode().ordinal(),
upstream.getSession().getAddress().getAddress().getHostAddress() upstream.getSession().getAddress().getAddress().getHostAddress(),
skinUploader.getId(),
skinUploader.getVerifyCode()
).toString()); ).toString());
} catch (Exception e) { } catch (Exception e) {
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
@ -570,13 +575,7 @@ public class GeyserSession implements CommandSender {
return; return;
} }
byte[] rawSkin = clientData.getAndTransformImage("Skin").encode(); String finalDataString = new String(encryptedData, StandardCharsets.UTF_8);
byte[] finalData = new byte[encryptedData.length + rawSkin.length + 1];
System.arraycopy(encryptedData, 0, finalData, 0, encryptedData.length);
finalData[encryptedData.length] = 0x21; // splitter
System.arraycopy(rawSkin, 0, finalData, encryptedData.length + 1, rawSkin.length);
String finalDataString = new String(finalData, StandardCharsets.UTF_8);
HandshakePacket handshakePacket = event.getPacket(); HandshakePacket handshakePacket = event.getPacket();
event.setPacket(new HandshakePacket( event.setPacket(new HandshakePacket(
@ -639,6 +638,10 @@ public class GeyserSession implements CommandSender {
if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
SkinManager.handleBedrockSkin(playerEntity, clientData); SkinManager.handleBedrockSkin(playerEntity, clientData);
} }
// We'll send the skin upload a bit after the handshake packet (aka this packet),
// because otherwise the global server returns the data too fast.
getAuthData().upload(connector);
} }
PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this); PacketTranslatorRegistry.JAVA_TRANSLATOR.translate(event.getPacket().getClass(), event.getPacket(), GeyserSession.this);

View file

@ -25,16 +25,26 @@
package org.geysermc.connector.network.session.auth; package org.geysermc.connector.network.session.auth;
import lombok.AllArgsConstructor; import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.geysermc.connector.GeyserConnector;
import java.util.UUID; import java.util.UUID;
@Getter @RequiredArgsConstructor
@AllArgsConstructor
public class AuthData { public class AuthData {
@Getter private final String name;
@Getter private final UUID UUID;
@Getter private final String xboxUUID;
private String name; private final JsonNode certChainData;
private UUID UUID; private final String clientData;
private String xboxUUID;
public void upload(GeyserConnector connector) {
// we can't upload the skin in LoginEncryptionUtil since the global server would return
// the skin too fast, that's why we upload it after we know for sure that the target server
// is ready to handle the result of the global server
connector.getSkinUploader().uploadSkin(certChainData, clientData);
}
} }

View file

@ -34,10 +34,8 @@ import lombok.Getter;
import org.geysermc.connector.skin.SkinProvider; import org.geysermc.connector.skin.SkinProvider;
import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.DeviceOs;
import org.geysermc.floodgate.util.InputMode; import org.geysermc.floodgate.util.InputMode;
import org.geysermc.floodgate.util.RawSkin;
import org.geysermc.floodgate.util.UiProfile; import org.geysermc.floodgate.util.UiProfile;
import java.awt.image.BufferedImage;
import java.util.Base64; import java.util.Base64;
import java.util.UUID; import java.util.UUID;
@ -115,77 +113,12 @@ public final class BedrockClientData {
@JsonProperty(value = "ThirdPartyNameOnly") @JsonProperty(value = "ThirdPartyNameOnly")
private boolean thirdPartyNameOnly; private boolean thirdPartyNameOnly;
private static RawSkin getLegacyImage(byte[] imageData, boolean alex) {
if (imageData == null) {
return null;
}
// width * height * 4 (rgba)
switch (imageData.length) {
case 8192:
return new RawSkin(64, 32, imageData, alex);
case 16384:
return new RawSkin(64, 64, imageData, alex);
case 32768:
return new RawSkin(64, 128, imageData, alex);
case 65536:
return new RawSkin(128, 128, imageData, alex);
default:
throw new IllegalArgumentException("Unknown legacy skin size");
}
}
public void setJsonData(JsonNode data) { public void setJsonData(JsonNode data) {
if (this.jsonData == null && data != null) { if (this.jsonData == null && data != null) {
this.jsonData = data; this.jsonData = data;
} }
} }
/**
* Taken from https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/network/protocol/LoginPacket.java<br>
* Internally only used for Skins, but can be used for Capes too
*/
public RawSkin getImage(String name) {
if (jsonData == null || !jsonData.has(name + "Data")) {
return null;
}
boolean alex = false;
if (name.equals("Skin")) {
alex = isAlex();
}
byte[] image = Base64.getDecoder().decode(jsonData.get(name + "Data").asText());
if (jsonData.has(name + "ImageWidth") && jsonData.has(name + "ImageHeight")) {
return new RawSkin(
jsonData.get(name + "ImageWidth").asInt(),
jsonData.get(name + "ImageHeight").asInt(),
image, alex
);
}
return getLegacyImage(image, alex);
}
public RawSkin getAndTransformImage(String name) {
RawSkin skin = getImage(name);
if (skin != null && (skin.width > 64 || skin.height > 64)) {
BufferedImage scaledImage =
SkinProvider.imageDataToBufferedImage(skin.data, skin.width, skin.height);
int max = Math.max(skin.width, skin.height);
while (max > 64) {
max /= 2;
scaledImage = SkinProvider.scale(scaledImage);
}
byte[] skinData = SkinProvider.bufferedImageToImageData(scaledImage);
skin.width = scaledImage.getWidth();
skin.height = scaledImage.getHeight();
skin.data = skinData;
}
return skin;
}
public boolean isAlex() { public boolean isAlex() {
try { try {
byte[] bytes = Base64.getDecoder().decode(geometryName.getBytes(Charsets.UTF_8)); byte[] bytes = Base64.getDecoder().decode(geometryName.getBytes(Charsets.UTF_8));

View file

@ -0,0 +1,205 @@
/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.skin;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.GeyserLogger;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.Constants;
import org.geysermc.connector.utils.PluginMessageUtils;
import org.geysermc.floodgate.util.WebsocketEventType;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import static org.geysermc.connector.utils.PluginMessageUtils.getSkinChannel;
public final class FloodgateSkinUploader {
private final ObjectMapper JACKSON = new ObjectMapper();
private final List<String> skinQueue = new ArrayList<>();
private final GeyserLogger logger;
private final WebSocketClient client;
@Getter private int id;
@Getter private String verifyCode;
@Getter private int subscribersCount;
public FloodgateSkinUploader(GeyserConnector connector) {
this.logger = connector.getLogger();
this.client = new WebSocketClient(Constants.SKIN_UPLOAD_URI) {
@Override
public void onOpen(ServerHandshake handshake) {
setConnectionLostTimeout(11);
Iterator<String> queueIterator = skinQueue.iterator();
while (isOpen() && queueIterator.hasNext()) {
send(queueIterator.next());
queueIterator.remove();
}
}
@Override
public void onMessage(String message) {
System.out.println(message);
// The reason why I don't like Jackson
try {
JsonNode node = JACKSON.readTree(message);
if (node.has("error")) {
logger.error("Got an error: " + node.get("error").asText());
return;
}
int typeId = node.get("event_id").asInt();
WebsocketEventType type = WebsocketEventType.getById(typeId);
if (type == null) {
logger.warning(String.format(
"Got (unknown) type %s. Ensure that Geyser is on the latest version and report this issue!",
typeId));
return;
}
switch (type) {
case SUBSCRIBER_CREATED:
id = node.get("id").asInt();
verifyCode = node.get("verify_code").asText();
break;
case SUBSCRIBERS_COUNT:
subscribersCount = node.get("subscribers_count").asInt();
break;
case SKIN_UPLOADED:
// if Geyser is the only subscriber we have send it to the server manually
if (subscribersCount != 1) {
break;
}
String xuid = node.get("xuid").asText();
String value = node.get("value").asText();
String signature = node.get("signature").asText();
;
GeyserSession session = connector.getPlayerByXuid(xuid);
if (session != null) {
byte[] bytes = (value + '\0' + signature)
.getBytes(StandardCharsets.UTF_8);
PluginMessageUtils.sendMessage(session, getSkinChannel(), bytes);
}
break;
}
} catch (Exception e) {
logger.error("Error while receiving a message", e);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
if (reason != null && !reason.isEmpty()) {
// The reason why I don't like Jackson
try {
JsonNode node = JACKSON.readTree(reason);
// info means that the uploader itself did nothing wrong
if (node.has("info")) {
String info = node.get("info").asText();
logger.debug("Got disconnected from the skin uploader: " + info);
}
// error means that the uploader did something wrong
if (node.has("error")) {
String error = node.get("error").asText();
logger.info("Got disconnected from the skin uploader: " + error);
}
// it can't be something else then info or error, so we won't handle anything other than that.
// try to reconnect (which will make a new id and verify token) after a few seconds
reconnectLater(connector);
} catch (Exception e) {
logger.error("Error while handling onClose", e);
}
}
}
@Override
public void onError(Exception ex) {
logger.error("Got an error", ex);
}
};
}
public void uploadSkin(JsonNode chainData, String clientData) {
if (chainData == null || !chainData.isArray() || clientData == null) {
return;
}
ObjectNode node = JACKSON.createObjectNode();
node.set("chain_data", chainData);
node.put("client_data", clientData);
// The reason why I don't like Jackson
String jsonString;
try {
jsonString = JACKSON.writeValueAsString(node);
} catch (Exception e) {
logger.error("Failed to upload skin", e);
return;
}
if (client.isOpen()) {
client.send(jsonString);
return;
}
skinQueue.add(jsonString);
}
private void reconnectLater(GeyserConnector connector) {
long additionalTime = ThreadLocalRandom.current().nextInt(7);
connector.getGeneralThreadPool().schedule(() -> {
try {
if (!client.connectBlocking()) {
reconnectLater(connector);
}
} catch (InterruptedException ignored) {
reconnectLater(connector);
}
}, 8 + additionalTime, TimeUnit.SECONDS);
}
public FloodgateSkinUploader start() {
client.connect();
return this;
}
public void stop() {
client.close();
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.utils;
import java.net.URI;
import java.net.URISyntaxException;
public final class Constants {
public static final URI SKIN_UPLOAD_URI;
static {
URI skinUploadUri = null;
try {
skinUploadUri = new URI("wss://api.geysermc.org/ws");
} catch (URISyntaxException e) {
e.printStackTrace();
}
SKIN_UPLOAD_URI = skinUploadUri;
}
}

View file

@ -118,7 +118,8 @@ public class LoginEncryptionUtils {
session.setAuthenticationData(new AuthData( session.setAuthenticationData(new AuthData(
extraData.get("displayName").asText(), extraData.get("displayName").asText(),
UUID.fromString(extraData.get("identity").asText()), UUID.fromString(extraData.get("identity").asText()),
extraData.get("XUID").asText() extraData.get("XUID").asText(),
certChainData, clientData
)); ));
if (payload.get("identityPublicKey").getNodeType() != JsonNodeType.STRING) { if (payload.get("identityPublicKey").getNodeType() != JsonNodeType.STRING) {

View file

@ -25,12 +25,15 @@
package org.geysermc.connector.utils; package org.geysermc.connector.utils;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerPluginMessagePacket;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class PluginMessageUtils { public class PluginMessageUtils {
private static final String SKIN_CHANNEL = "floodgate:skin";
private static final byte[] GEYSER_BRAND_DATA; private static final byte[] GEYSER_BRAND_DATA;
private static final byte[] FLOODGATE_REGISTER_DATA; private static final byte[] FLOODGATE_REGISTER_DATA;
@ -42,7 +45,7 @@ public class PluginMessageUtils {
.put(data) .put(data)
.array(); .array();
FLOODGATE_REGISTER_DATA = "floodgate:skin\0floodgate:form".getBytes(Charsets.UTF_8); FLOODGATE_REGISTER_DATA = (SKIN_CHANNEL + "\0floodgate:form").getBytes(Charsets.UTF_8);
} }
/** /**
@ -63,6 +66,22 @@ public class PluginMessageUtils {
return FLOODGATE_REGISTER_DATA; return FLOODGATE_REGISTER_DATA;
} }
/**
* Returns the skin channel used in Floodgate
*/
public static String getSkinChannel() {
return SKIN_CHANNEL;
}
public static void sendMessage(GeyserSession session, String channel, byte[] data) {
byte[] finalData =
ByteBuffer.allocate(data.length + getVarIntLength(data.length))
.put(writeVarInt(data.length))
.put(data)
.array();
session.sendDownstreamPacket(new ServerPluginMessagePacket(channel, finalData));
}
private static byte[] writeVarInt(int value) { private static byte[] writeVarInt(int value) {
byte[] data = new byte[getVarIntLength(value)]; byte[] data = new byte[getVarIntLength(value)];
int index = 0; int index = 0;