diff --git a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java index 5f449fe2d..4ea5ee78e 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java +++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java @@ -37,7 +37,7 @@ import lombok.Getter; @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) 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 username; @@ -50,19 +50,24 @@ public final class BedrockData implements Cloneable { private final LinkedPlayer linkedPlayer; private final boolean fromProxy; + private final int subscribeId; + private final String verifyCode; + private final int dataLength; public static BedrockData of(String version, String username, String xuid, int deviceOs, 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, - 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, - String languageCode, int uiProfile, int inputMode, String ip) { - return of(version, username, xuid, deviceOs, languageCode, - uiProfile, inputMode, ip, null, false); + String languageCode, int uiProfile, int inputMode, String ip, + int subscribeId, String verifyCode) { + return of(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null, + false, subscribeId, verifyCode); } 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 return new BedrockData( split[0], split[1], split[2], Integer.parseInt(split[3]), split[4], - Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], - linkedPlayer, "1".equals(split[8]), split.length + Integer.parseInt(split[5]), Integer.parseInt(split[6]), split[7], linkedPlayer, + "1".equals(split[9]), Integer.parseInt(split[10]), split[11], split.length ); } 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() { @@ -94,7 +100,8 @@ public final class BedrockData implements Cloneable { return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' + languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' + (fromProxy ? 1 : 0) + '\0' + - (linkedPlayer != null ? linkedPlayer.toString() : "null"); + (linkedPlayer != null ? linkedPlayer.toString() : "null") + '\0' + + subscribeId + '\0' + verifyCode; } @Override diff --git a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java b/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java deleted file mode 100644 index 470b7e24a..000000000 --- a/common/src/main/java/org/geysermc/floodgate/util/RawSkin.java +++ /dev/null @@ -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()); - } -} diff --git a/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java b/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java new file mode 100644 index 000000000..1527f2360 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java @@ -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; + } +} diff --git a/connector/pom.xml b/connector/pom.xml index 5e78fcfc3..04fc918fa 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -22,6 +22,11 @@ 2.9.8 compile + + org.java-websocket + Java-Websocket + 1.5.1 + com.fasterxml.jackson.datatype jackson-datatype-jsr310 diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 38c2aa29a..4dc78df1c 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -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.entity.BlockEntityTranslator; 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.LanguageUtils; import org.geysermc.connector.utils.LocaleUtils; @@ -108,6 +109,7 @@ public class GeyserConnector { private AuthType authType; private FloodgateCipher cipher; + private FloodgateSkinUploader skinUploader; private boolean shuttingDown = false; @@ -203,6 +205,7 @@ public class GeyserConnector { cipher = new AesCipher(new Base64Topping()); cipher.init(key); logger.info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); + skinUploader = new FloodgateSkinUploader(this).start(); } catch (Exception exception) { logger.severe(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), exception); } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index 8bdb7f2b1..722cd2e45 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -67,10 +67,10 @@ import lombok.Getter; import lombok.NonNull; import lombok.Setter; import org.geysermc.connector.GeyserConnector; -import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.common.AuthType; 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.SkullPlayerEntity; 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.inventory.EnchantmentInventoryTranslator; import org.geysermc.connector.network.translators.item.ItemRegistry; +import org.geysermc.connector.skin.FloodgateSkinUploader; import org.geysermc.connector.skin.SkinManager; import org.geysermc.connector.utils.*; import org.geysermc.cumulus.Form; @@ -553,7 +554,9 @@ public class GeyserSession implements CommandSender { byte[] encryptedData; try { + FloodgateSkinUploader skinUploader = connector.getSkinUploader(); FloodgateCipher cipher = connector.getCipher(); + encryptedData = cipher.encryptFromString(BedrockData.of( clientData.getGameVersion(), authData.getName(), @@ -562,7 +565,9 @@ public class GeyserSession implements CommandSender { clientData.getLanguageCode(), clientData.getUiProfile().ordinal(), clientData.getCurrentInputMode().ordinal(), - upstream.getSession().getAddress().getAddress().getHostAddress() + upstream.getSession().getAddress().getAddress().getHostAddress(), + skinUploader.getId(), + skinUploader.getVerifyCode() ).toString()); } catch (Exception e) { connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); @@ -570,13 +575,7 @@ public class GeyserSession implements CommandSender { return; } - byte[] rawSkin = clientData.getAndTransformImage("Skin").encode(); - 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); + String finalDataString = new String(encryptedData, StandardCharsets.UTF_8); HandshakePacket handshakePacket = event.getPacket(); event.setPacket(new HandshakePacket( @@ -639,6 +638,10 @@ public class GeyserSession implements CommandSender { if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { 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); diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java index ba9e13548..463276891 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java @@ -25,16 +25,26 @@ package org.geysermc.connector.network.session.auth; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.geysermc.connector.GeyserConnector; import java.util.UUID; -@Getter -@AllArgsConstructor +@RequiredArgsConstructor public class AuthData { + @Getter private final String name; + @Getter private final UUID UUID; + @Getter private final String xboxUUID; - private String name; - private UUID UUID; - private String xboxUUID; + private final JsonNode certChainData; + private final String clientData; + + 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); + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java index 7a7069d07..aed98b09c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/auth/BedrockClientData.java @@ -34,10 +34,8 @@ import lombok.Getter; import org.geysermc.connector.skin.SkinProvider; import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.InputMode; -import org.geysermc.floodgate.util.RawSkin; import org.geysermc.floodgate.util.UiProfile; -import java.awt.image.BufferedImage; import java.util.Base64; import java.util.UUID; @@ -115,77 +113,12 @@ public final class BedrockClientData { @JsonProperty(value = "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) { if (this.jsonData == null && data != null) { this.jsonData = data; } } - /** - * Taken from https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/network/protocol/LoginPacket.java
- * 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() { try { byte[] bytes = Base64.getDecoder().decode(geometryName.getBytes(Charsets.UTF_8)); diff --git a/connector/src/main/java/org/geysermc/connector/skin/FloodgateSkinUploader.java b/connector/src/main/java/org/geysermc/connector/skin/FloodgateSkinUploader.java new file mode 100644 index 000000000..e710a3591 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/skin/FloodgateSkinUploader.java @@ -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 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 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(); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/Constants.java b/connector/src/main/java/org/geysermc/connector/utils/Constants.java new file mode 100644 index 000000000..8dcd63d3e --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/Constants.java @@ -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; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java index 00c7aea0f..f68ee0e4c 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -118,7 +118,8 @@ public class LoginEncryptionUtils { session.setAuthenticationData(new AuthData( extraData.get("displayName").asText(), UUID.fromString(extraData.get("identity").asText()), - extraData.get("XUID").asText() + extraData.get("XUID").asText(), + certChainData, clientData )); if (payload.get("identityPublicKey").getNodeType() != JsonNodeType.STRING) { diff --git a/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java index 4e0fd6074..1569002f9 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java @@ -25,12 +25,15 @@ package org.geysermc.connector.utils; +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerPluginMessagePacket; import com.google.common.base.Charsets; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; import java.nio.ByteBuffer; public class PluginMessageUtils { + private static final String SKIN_CHANNEL = "floodgate:skin"; private static final byte[] GEYSER_BRAND_DATA; private static final byte[] FLOODGATE_REGISTER_DATA; @@ -42,7 +45,7 @@ public class PluginMessageUtils { .put(data) .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; } + /** + * 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) { byte[] data = new byte[getVarIntLength(value)]; int index = 0;