diff --git a/Jenkinsfile b/Jenkinsfile index 09e88e86e..d76a6f737 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -22,7 +22,10 @@ pipeline { stage ('Deploy') { when { - branch "master" + anyOf { + branch "master" + branch "floodgate-2.0" + } } steps { diff --git a/bootstrap/bungeecord/pom.xml b/bootstrap/bungeecord/pom.xml index 9cf8b4763..5e15162b8 100644 --- a/bootstrap/bungeecord/pom.xml +++ b/bootstrap/bungeecord/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-bungeecord @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java index af246f6f1..dc29b4bde 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeeConfiguration.java @@ -41,7 +41,7 @@ public final class GeyserBungeeConfiguration extends GeyserJacksonConfiguration private Path floodgateKeyPath; public void loadFloodgate(GeyserBungeePlugin plugin) { - Plugin floodgate = plugin.getProxy().getPluginManager().getPlugin("floodgate-bungee"); + Plugin floodgate = plugin.getProxy().getPluginManager().getPlugin("floodgate"); Path geyserDataFolder = plugin.getDataFolder().toPath(); Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java index 8d37bffb1..c99a47621 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/GeyserBungeePlugin.java @@ -94,10 +94,10 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { this.geyserLogger = new GeyserBungeeLogger(getLogger(), geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - if (geyserConfig.getRemote().getAuthType().equals("floodgate") && getProxy().getPluginManager().getPlugin("floodgate-bungee") == null) { + if (geyserConfig.getRemote().getAuthType().equals("floodgate") && getProxy().getPluginManager().getPlugin("floodgate") == null) { geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); return; - } else if (geyserConfig.isAutoconfiguredRemote() && getProxy().getPluginManager().getPlugin("floodgate-bungee") != null) { + } else if (geyserConfig.isAutoconfiguredRemote() && getProxy().getPluginManager().getPlugin("floodgate") != null) { // Floodgate installed means that the user wants Floodgate authentication geyserLogger.debug("Auto-setting to Floodgate authentication."); geyserConfig.getRemote().setAuthType("floodgate"); diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml index b06fc59cf..d58cb2ca4 100644 --- a/bootstrap/pom.xml +++ b/bootstrap/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-parent pom diff --git a/bootstrap/spigot/pom.xml b/bootstrap/spigot/pom.xml index 9bd8d1e49..c4286be9f 100644 --- a/bootstrap/spigot/pom.xml +++ b/bootstrap/spigot/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-spigot @@ -21,7 +21,7 @@ org.geysermc connector - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java index 45b1d2538..e39c50bca 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotConfiguration.java @@ -42,7 +42,7 @@ public final class GeyserSpigotConfiguration extends GeyserJacksonConfiguration private Path floodgateKeyPath; public void loadFloodgate(GeyserSpigotPlugin plugin) { - Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate-bukkit"); + Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); Path geyserDataFolder = plugin.getDataFolder().toPath(); Path floodgateDataFolder = floodgate != null ? floodgate.getDataFolder().toPath() : null; diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java index 6004c0d82..985d6ed0b 100644 --- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java +++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/GeyserSpigotPlugin.java @@ -120,11 +120,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { this.geyserLogger = new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); - if (geyserConfig.getRemote().getAuthType().equals("floodgate") && Bukkit.getPluginManager().getPlugin("floodgate-bukkit") == null) { + if (geyserConfig.getRemote().getAuthType().equals("floodgate") && Bukkit.getPluginManager().getPlugin("floodgate") == null) { geyserLogger.severe(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.disabling")); this.getPluginLoader().disablePlugin(this); return; - } else if (geyserConfig.isAutoconfiguredRemote() && Bukkit.getPluginManager().getPlugin("floodgate-bukkit") != null) { + } else if (geyserConfig.isAutoconfiguredRemote() && Bukkit.getPluginManager().getPlugin("floodgate") != null) { // Floodgate installed means that the user wants Floodgate authentication geyserLogger.debug("Auto-setting to Floodgate authentication."); geyserConfig.getRemote().setAuthType("floodgate"); diff --git a/bootstrap/spigot/src/main/resources/plugin.yml b/bootstrap/spigot/src/main/resources/plugin.yml index fee71ab1f..ea655f902 100644 --- a/bootstrap/spigot/src/main/resources/plugin.yml +++ b/bootstrap/spigot/src/main/resources/plugin.yml @@ -3,7 +3,7 @@ name: ${outputName}-Spigot author: ${project.organization.name} website: ${project.organization.url} version: ${project.version} -softdepend: ["ViaVersion"] +softdepend: ["ViaVersion", "floodgate"] api-version: 1.13 commands: geyser: diff --git a/bootstrap/sponge/pom.xml b/bootstrap/sponge/pom.xml index 93ae3ab6b..adeaa91de 100644 --- a/bootstrap/sponge/pom.xml +++ b/bootstrap/sponge/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-sponge @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/bootstrap/standalone/pom.xml b/bootstrap/standalone/pom.xml index 122991357..6610af3b5 100644 --- a/bootstrap/standalone/pom.xml +++ b/bootstrap/standalone/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-standalone @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/bootstrap/velocity/pom.xml b/bootstrap/velocity/pom.xml index b9f2e64f2..3b093285d 100644 --- a/bootstrap/velocity/pom.xml +++ b/bootstrap/velocity/pom.xml @@ -6,7 +6,7 @@ org.geysermc bootstrap-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT bootstrap-velocity @@ -14,7 +14,7 @@ org.geysermc connector - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT compile diff --git a/common/pom.xml b/common/pom.xml index e40e40c55..be90f0146 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -6,22 +6,20 @@ org.geysermc geyser-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT common - com.google.code.gson - gson - 2.8.2 - compile + org.geysermc.cumulus + cumulus + 1.0-SNAPSHOT - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.9.8 - compile + com.google.code.gson + gson + 2.8.6 \ No newline at end of file diff --git a/common/src/main/java/org/geysermc/common/PlatformType.java b/common/src/main/java/org/geysermc/common/PlatformType.java index a1096f347..098fd3946 100644 --- a/common/src/main/java/org/geysermc/common/PlatformType.java +++ b/common/src/main/java/org/geysermc/common/PlatformType.java @@ -31,7 +31,6 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum PlatformType { - ANDROID("Android"), BUNGEECORD("BungeeCord"), FABRIC("Fabric"), diff --git a/common/src/main/java/org/geysermc/common/window/CustomFormWindow.java b/common/src/main/java/org/geysermc/common/window/CustomFormWindow.java deleted file mode 100644 index 7043dda9f..000000000 --- a/common/src/main/java/org/geysermc/common/window/CustomFormWindow.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.common.window; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; -import lombok.Setter; -import org.geysermc.common.window.button.FormImage; -import org.geysermc.common.window.component.*; -import org.geysermc.common.window.response.CustomFormResponse; -import org.geysermc.common.window.response.FormResponseData; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class CustomFormWindow extends FormWindow { - - @Getter - @Setter - private String title; - - @Getter - @Setter - private FormImage icon; - - @Getter - private List content; - - public CustomFormWindow(String title) { - this(title, new ArrayList<>()); - } - - public CustomFormWindow(String title, List content) { - this(title, content, (FormImage) null); - } - - public CustomFormWindow(String title, List content, String icon) { - this(title, content, new FormImage(FormImage.FormImageType.URL, icon)); - } - - public CustomFormWindow(String title, List content, FormImage icon) { - super("custom_form"); - - this.title = title; - this.content = content; - this.icon = icon; - } - - public void addComponent(FormComponent component) { - content.add(component); - } - - public String getJSONData() { - String toModify = ""; - try { - toModify = new ObjectMapper().writeValueAsString(this); - } catch (JsonProcessingException e) { } - - //We need to replace this due to Java not supporting declaring class field 'default' - return toModify.replace("defaultOptionIndex", "default") - .replace("defaultText", "default") - .replace("defaultValue", "default") - .replace("defaultStepIndex", "default"); - } - - public void setResponse(String data) { - if (data == null || data.trim().equalsIgnoreCase("null") || data.isEmpty()) { - closed = true; - return; - } - - int i = 0; - Map dropdownResponses = new HashMap(); - Map inputResponses = new HashMap(); - Map sliderResponses = new HashMap(); - Map stepSliderResponses = new HashMap(); - Map toggleResponses = new HashMap(); - Map responses = new HashMap(); - Map labelResponses = new HashMap(); - - List componentResponses = new ArrayList<>(); - try { - componentResponses = new ObjectMapper().readValue(data.trim(), new TypeReference>(){}); - } catch (IOException e) { } - - for (String response : componentResponses) { - if (i >= content.size()) { - break; - } - - FormComponent component = content.get(i); - if (component == null) - return; - - if (component instanceof LabelComponent) { - LabelComponent labelComponent = (LabelComponent) component; - labelResponses.put(i, labelComponent.getText()); - } - - if (component instanceof DropdownComponent) { - DropdownComponent dropdownComponent = (DropdownComponent) component; - String option = dropdownComponent.getOptions().get(Integer.parseInt(response)); - - dropdownResponses.put(i, new FormResponseData(Integer.parseInt(response), option)); - responses.put(i, option); - } - - if (component instanceof InputComponent) { - inputResponses.put(i, response); - responses.put(i, response); - } - - if (component instanceof SliderComponent) { - float value = Float.parseFloat(response); - sliderResponses.put(i, value); - responses.put(i, value); - } - - if (component instanceof StepSliderComponent) { - StepSliderComponent stepSliderComponent = (StepSliderComponent) component; - String step = stepSliderComponent.getSteps().get(Integer.parseInt(response)); - stepSliderResponses.put(i, new FormResponseData(Integer.parseInt(response), step)); - responses.put(i, step); - } - - if (component instanceof ToggleComponent) { - boolean answer = Boolean.parseBoolean(response); - toggleResponses.put(i, answer); - responses.put(i, answer); - } - i++; - } - - this.response = new CustomFormResponse(responses, dropdownResponses, inputResponses, - sliderResponses, stepSliderResponses, toggleResponses, labelResponses); - } -} diff --git a/common/src/main/java/org/geysermc/common/window/FormWindow.java b/common/src/main/java/org/geysermc/common/window/FormWindow.java deleted file mode 100644 index efc06fe80..000000000 --- a/common/src/main/java/org/geysermc/common/window/FormWindow.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.common.window; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; -import lombok.Setter; -import org.geysermc.common.window.response.FormResponse; - -public abstract class FormWindow { - - @Getter - private final String type; - - @Getter - protected FormResponse response; - - @Getter - @Setter - protected boolean closed; - - public FormWindow(String type) { - this.type = type; - } - - // Lombok won't work here, so we need to make our own method - public void setResponse(FormResponse response) { - this.response = response; - } - - @JsonIgnore - public abstract String getJSONData(); - - public abstract void setResponse(String response); - -} diff --git a/common/src/main/java/org/geysermc/common/window/ModalFormWindow.java b/common/src/main/java/org/geysermc/common/window/ModalFormWindow.java deleted file mode 100644 index 9d8021617..000000000 --- a/common/src/main/java/org/geysermc/common/window/ModalFormWindow.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.common.window; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; -import lombok.Setter; -import org.geysermc.common.window.response.ModalFormResponse; - -public class ModalFormWindow extends FormWindow { - - @Getter - @Setter - private String title; - - @Getter - @Setter - private String content; - - @Getter - @Setter - private String button1; - - @Getter - @Setter - private String button2; - - public ModalFormWindow(String title, String content, String button1, String button2) { - super("modal"); - - this.title = title; - this.content = content; - this.button1 = button1; - this.button2 = button2; - } - - @Override - public String getJSONData() { - try { - return new ObjectMapper().writeValueAsString(this); - } catch (JsonProcessingException e) { - return ""; - } - } - - public void setResponse(String data) { - if (data == null || data.equalsIgnoreCase("null")) { - closed = true; - return; - } - - if (Boolean.parseBoolean(data)) { - response = new ModalFormResponse(0, button1); - } else { - response = new ModalFormResponse(1, button2); - } - } -} diff --git a/common/src/main/java/org/geysermc/common/window/SimpleFormWindow.java b/common/src/main/java/org/geysermc/common/window/SimpleFormWindow.java deleted file mode 100644 index 48bda0bd2..000000000 --- a/common/src/main/java/org/geysermc/common/window/SimpleFormWindow.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.common.window; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; -import lombok.Setter; -import org.geysermc.common.window.button.FormButton; -import org.geysermc.common.window.response.SimpleFormResponse; - -import java.util.ArrayList; -import java.util.List; - - -public class SimpleFormWindow extends FormWindow { - - @Getter - @Setter - private String title; - - @Getter - @Setter - private String content; - - @Getter - @Setter - private List buttons; - - public SimpleFormWindow(String title, String content) { - this(title, content, new ArrayList()); - } - - public SimpleFormWindow(String title, String content, List buttons) { - super("form"); - - this.title = title; - this.content = content; - this.buttons = buttons; - } - - @Override - public String getJSONData() { - try { - return new ObjectMapper().writeValueAsString(this); - } catch (JsonProcessingException e) { - return ""; - } - } - - public void setResponse(String data) { - if (data == null || data.trim().equalsIgnoreCase("null")) { - closed = true; - return; - } - - int buttonID; - try { - buttonID = Integer.parseInt(data.trim()); - } catch (Exception ex) { - return; - } - - if (buttonID >= buttons.size()) { - response = new SimpleFormResponse(buttonID, null); - return; - } - - response = new SimpleFormResponse(buttonID, buttons.get(buttonID)); - } -} diff --git a/common/src/main/java/org/geysermc/common/window/component/DropdownComponent.java b/common/src/main/java/org/geysermc/common/window/component/DropdownComponent.java deleted file mode 100644 index 8ba17bb4b..000000000 --- a/common/src/main/java/org/geysermc/common/window/component/DropdownComponent.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.common.window.component; - -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -public class DropdownComponent extends FormComponent { - - @Getter - @Setter - private String text; - - @Getter - @Setter - private List options; - - @Getter - @Setter - private int defaultOptionIndex; - - public DropdownComponent() { - super("dropdown"); - } - - public void addOption(String option, boolean isDefault) { - options.add(option); - if (isDefault) - defaultOptionIndex = options.size() - 1; - } -} diff --git a/common/src/main/java/org/geysermc/common/window/component/InputComponent.java b/common/src/main/java/org/geysermc/common/window/component/InputComponent.java deleted file mode 100644 index 9dfc249a2..000000000 --- a/common/src/main/java/org/geysermc/common/window/component/InputComponent.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.common.window.component; - -import lombok.Getter; -import lombok.Setter; - -public class InputComponent extends FormComponent { - - @Getter - @Setter - private String text; - - @Getter - @Setter - private String placeholder; - - @Getter - @Setter - private String defaultText; - - public InputComponent(String text, String placeholder, String defaultText) { - super("input"); - - this.text = text; - this.placeholder = placeholder; - this.defaultText = defaultText; - } -} diff --git a/common/src/main/java/org/geysermc/common/window/component/SliderComponent.java b/common/src/main/java/org/geysermc/common/window/component/SliderComponent.java deleted file mode 100644 index 3e681664c..000000000 --- a/common/src/main/java/org/geysermc/common/window/component/SliderComponent.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.common.window.component; - -import lombok.Getter; -import lombok.Setter; - -public class SliderComponent extends FormComponent { - - @Getter - @Setter - private String text; - - @Getter - @Setter - private float min; - - @Getter - @Setter - private float max; - - @Getter - @Setter - private int step; - - @Getter - @Setter - private float defaultValue; - - public SliderComponent(String text, float min, float max, int step, float defaultValue) { - super("slider"); - - this.text = text; - this.min = Math.max(min, 0f); - this.max = max > this.min ? max : this.min; - if (step != -1f && step > 0) - this.step = step; - - if (defaultValue != -1f) - this.defaultValue = defaultValue; - } -} diff --git a/common/src/main/java/org/geysermc/common/window/component/StepSliderComponent.java b/common/src/main/java/org/geysermc/common/window/component/StepSliderComponent.java deleted file mode 100644 index 8e6748a94..000000000 --- a/common/src/main/java/org/geysermc/common/window/component/StepSliderComponent.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.common.window.component; - -import lombok.Getter; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.List; - -public class StepSliderComponent extends FormComponent { - - @Getter - @Setter - private String text; - - @Getter - private List steps; - - @Getter - @Setter - private int defaultStepIndex; - - public StepSliderComponent(String text) { - this(text, new ArrayList()); - } - - public StepSliderComponent(String text, List steps) { - this(text, steps, 0); - } - - public StepSliderComponent(String text, List steps, int defaultStepIndex) { - super("step_slider"); - - this.text = text; - this.steps = steps; - this.defaultStepIndex = defaultStepIndex; - } - - public void addStep(String step, boolean isDefault) { - steps.add(step); - - if (isDefault) - defaultStepIndex = steps.size() - 1; - } -} diff --git a/common/src/main/java/org/geysermc/common/window/component/ToggleComponent.java b/common/src/main/java/org/geysermc/common/window/component/ToggleComponent.java deleted file mode 100644 index e79e7592a..000000000 --- a/common/src/main/java/org/geysermc/common/window/component/ToggleComponent.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.common.window.component; - -import lombok.Getter; -import lombok.Setter; - -public class ToggleComponent extends FormComponent { - - @Getter - @Setter - private String text; - - @Getter - @Setter - private boolean defaultValue; - - public ToggleComponent(String text) { - this(text, false); - } - - public ToggleComponent(String text, boolean defaultValue) { - super("toggle"); - - this.text = text; - this.defaultValue = defaultValue; - } -} diff --git a/common/src/main/java/org/geysermc/common/window/response/CustomFormResponse.java b/common/src/main/java/org/geysermc/common/window/response/CustomFormResponse.java deleted file mode 100644 index 252a0978b..000000000 --- a/common/src/main/java/org/geysermc/common/window/response/CustomFormResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.common.window.response; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.geysermc.common.window.response.FormResponse; -import org.geysermc.common.window.response.FormResponseData; - -import java.util.Map; - -@Getter -@AllArgsConstructor -public class CustomFormResponse implements FormResponse { - - private Map responses; - private Map dropdownResponses; - private Map inputResponses; - private Map sliderResponses; - private Map stepSliderResponses; - private Map toggleResponses; - private Map labelResponses; -} diff --git a/common/src/main/java/org/geysermc/common/window/response/SimpleFormResponse.java b/common/src/main/java/org/geysermc/common/window/response/SimpleFormResponse.java deleted file mode 100644 index c24f72d02..000000000 --- a/common/src/main/java/org/geysermc/common/window/response/SimpleFormResponse.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.common.window.response; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.geysermc.common.window.button.FormButton; -import org.geysermc.common.window.response.FormResponse; - -@Getter -@AllArgsConstructor -public class SimpleFormResponse implements FormResponse { - - private int clickedButtonId; - private FormButton clickedButton; -} diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/AesCipher.java b/common/src/main/java/org/geysermc/floodgate/crypto/AesCipher.java new file mode 100644 index 000000000..f602f4be0 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/crypto/AesCipher.java @@ -0,0 +1,125 @@ +/* + * 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/Floodgate + * + */ + +package org.geysermc.floodgate.crypto; + +import lombok.RequiredArgsConstructor; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.security.Key; +import java.security.SecureRandom; + +@RequiredArgsConstructor +public final class AesCipher implements FloodgateCipher { + public static final int IV_LENGTH = 12; + private static final int TAG_BIT_LENGTH = 128; + private static final String CIPHER_NAME = "AES/GCM/NoPadding"; + + private final SecureRandom secureRandom = new SecureRandom(); + private final Topping topping; + private SecretKey secretKey; + + public void init(Key key) { + if (!"AES".equals(key.getAlgorithm())) { + throw new RuntimeException( + "Algorithm was expected to be AES, but got " + key.getAlgorithm() + ); + } + secretKey = (SecretKey) key; + } + + public byte[] encrypt(byte[] data) throws Exception { + Cipher cipher = Cipher.getInstance(CIPHER_NAME); + + byte[] iv = new byte[IV_LENGTH]; + secureRandom.nextBytes(iv); + + GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec); + byte[] cipherText = cipher.doFinal(data); + + if (topping != null) { + iv = topping.encode(iv); + cipherText = topping.encode(cipherText); + } + + return ByteBuffer.allocate(iv.length + cipherText.length + HEADER_LENGTH + 1) + .put(IDENTIFIER) // header + .put(iv) + .put((byte) 0x21) + .put(cipherText) + .array(); + } + + public byte[] decrypt(byte[] cipherTextWithIv) throws Exception { + checkHeader(cipherTextWithIv); + + Cipher cipher = Cipher.getInstance(CIPHER_NAME); + + int bufferLength = cipherTextWithIv.length - HEADER_LENGTH; + ByteBuffer buffer = ByteBuffer.wrap(cipherTextWithIv, HEADER_LENGTH, bufferLength); + + int ivLength = IV_LENGTH; + + if (topping != null) { + int mark = buffer.position(); + + // we need the first index, the second is for the optional RawSkin + boolean found = false; + while (buffer.hasRemaining() && !found) { + if (buffer.get() == 0x21) { + found = true; + } + } + + ivLength = buffer.position() - mark - 1; // don't include the splitter itself + // don't remove this cast, it'll cause problems if you remove it + ((Buffer) buffer).position(mark); // reset to the pre-while index + } + + byte[] iv = new byte[ivLength]; + buffer.get(iv); + + // don't remove this cast, it'll cause problems if you remove it + ((Buffer) buffer).position(buffer.position() + 1); // skip splitter + + byte[] cipherText = new byte[buffer.remaining()]; + buffer.get(cipherText); + + if (topping != null) { + iv = topping.decode(iv); + cipherText = topping.decode(cipherText); + } + + GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); + return cipher.doFinal(cipherText); + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/AesKeyProducer.java b/common/src/main/java/org/geysermc/floodgate/crypto/AesKeyProducer.java new file mode 100644 index 000000000..faec0ad10 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/crypto/AesKeyProducer.java @@ -0,0 +1,74 @@ +/* + * 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/Floodgate + * + */ + +package org.geysermc.floodgate.crypto; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public final class AesKeyProducer implements KeyProducer { + public static int KEY_SIZE = 128; + + @Override + public SecretKey produce() { + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(KEY_SIZE, getSecureRandom()); + return keyGenerator.generateKey(); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + @Override + public SecretKey produceFrom(byte[] keyFileData) { + try { + return new SecretKeySpec(keyFileData, "AES"); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + private SecureRandom getSecureRandom() throws NoSuchAlgorithmException { + // use Windows-PRNG for windows (default impl is SHA1PRNG) + if (System.getProperty("os.name").startsWith("Windows")) { + return SecureRandom.getInstance("Windows-PRNG"); + } else { + try { + // NativePRNG (which should be the default on unix-systems) can still block your + // system. Even though it isn't as bad as NativePRNGBlocking, we still try to + // prevent that if possible + return SecureRandom.getInstance("NativePRNGNonBlocking"); + } catch (NoSuchAlgorithmException ignored) { + // at this point we just have to go with the default impl even if it blocks + return new SecureRandom(); + } + } + } +} diff --git a/common/src/main/java/org/geysermc/common/window/response/ModalFormResponse.java b/common/src/main/java/org/geysermc/floodgate/crypto/Base64Topping.java similarity index 78% rename from common/src/main/java/org/geysermc/common/window/response/ModalFormResponse.java rename to common/src/main/java/org/geysermc/floodgate/crypto/Base64Topping.java index 3338df4da..18fc5a04a 100644 --- a/common/src/main/java/org/geysermc/common/window/response/ModalFormResponse.java +++ b/common/src/main/java/org/geysermc/floodgate/crypto/Base64Topping.java @@ -23,16 +23,18 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window.response; +package org.geysermc.floodgate.crypto; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.geysermc.common.window.response.FormResponse; +import java.util.Base64; -@Getter -@AllArgsConstructor -public class ModalFormResponse implements FormResponse { +public final class Base64Topping implements Topping { + @Override + public byte[] encode(byte[] data) { + return Base64.getEncoder().encode(data); + } - private int clickedButtonId; - private String clickedButtonText; + @Override + public byte[] decode(byte[] data) { + return Base64.getDecoder().decode(data); + } } diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java b/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java new file mode 100644 index 000000000..69c8f9f6d --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java @@ -0,0 +1,163 @@ +/* + * 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/Floodgate + * + */ + +package org.geysermc.floodgate.crypto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.geysermc.floodgate.util.InvalidFormatException; + +import java.nio.charset.StandardCharsets; +import java.security.Key; + +/** + * Responsible for both encrypting and decrypting data + */ +public interface FloodgateCipher { + // use invalid username characters at the beginning and the end of the identifier, + // to make sure that it doesn't get messed up with usernames + byte[] IDENTIFIER = "^Floodgate^".getBytes(StandardCharsets.UTF_8); + int HEADER_LENGTH = IDENTIFIER.length; + + static boolean hasHeader(String data) { + if (data.length() < IDENTIFIER.length) { + return false; + } + + for (int i = 0; i < IDENTIFIER.length; i++) { + if (IDENTIFIER[i] != data.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Initializes the instance by giving it the key it needs to encrypt or decrypt data + * + * @param key the key used to encrypt and decrypt data + */ + void init(Key key); + + /** + * Encrypts the given data using the Key provided in {@link #init(Key)} + * + * @param data the data to encrypt + * @return the encrypted data + * @throws Exception when the encryption failed + */ + byte[] encrypt(byte[] data) throws Exception; + + /** + * Encrypts data from a String.
This method internally calls {@link #encrypt(byte[])} + * + * @param data the data to encrypt + * @return the encrypted data + * @throws Exception when the encryption failed + */ + default byte[] encryptFromString(String data) throws Exception { + return encrypt(data.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Decrypts the given data using the Key provided in {@link #init(Key)} + * + * @param data the data to decrypt + * @return the decrypted data + * @throws Exception when the decrypting failed + */ + byte[] decrypt(byte[] data) throws Exception; + + /** + * Decrypts a byte[] and turn it into a String.
This method internally calls {@link + * #decrypt(byte[])} and converts the returned byte[] into a String. + * + * @param data the data to encrypt + * @return the decrypted data in a UTF-8 String + * @throws Exception when the decrypting failed + */ + default String decryptToString(byte[] data) throws Exception { + byte[] decrypted = decrypt(data); + if (decrypted == null) { + return null; + } + return new String(decrypted, StandardCharsets.UTF_8); + } + + /** + * Decrypts a String.
This method internally calls {@link #decrypt(byte[])} by converting + * the UTF-8 String into a byte[] + * + * @param data the data to decrypt + * @return the decrypted data in a byte[] + * @throws Exception when the decrypting failed + */ + default byte[] decryptFromString(String data) throws Exception { + return decrypt(data.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Checks if the header is valid. This method will throw an InvalidFormatException when the + * header is invalid. + * + * @param data the data to check + * @throws InvalidFormatException when the header is invalid + */ + default void checkHeader(byte[] data) throws InvalidFormatException { + final int identifierLength = IDENTIFIER.length; + + if (data.length <= HEADER_LENGTH) { + throw new InvalidFormatException("Data length is smaller then header." + + "Needed " + HEADER_LENGTH + ", got " + data.length, + true + ); + } + + for (int i = 0; i < identifierLength; i++) { + if (IDENTIFIER[i] != data[i]) { + StringBuilder receivedIdentifier = new StringBuilder(); + for (byte b : IDENTIFIER) { + receivedIdentifier.append(b); + } + + throw new InvalidFormatException( + String.format("Expected identifier %s, got %s", + new String(IDENTIFIER, StandardCharsets.UTF_8), + receivedIdentifier.toString() + ), + true + ); + } + } + } + + @Data + @AllArgsConstructor + class HeaderResult { + private int version; + private int startIndex; + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/KeyProducer.java b/common/src/main/java/org/geysermc/floodgate/crypto/KeyProducer.java new file mode 100644 index 000000000..fc2ac512d --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/crypto/KeyProducer.java @@ -0,0 +1,41 @@ +/* + * 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/Floodgate + * + */ + +package org.geysermc.floodgate.crypto; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.Key; + +public interface KeyProducer { + Key produce(); + Key produceFrom(byte[] keyFileData); + + default Key produceFrom(Path keyFileLocation) throws IOException { + return produceFrom(Files.readAllBytes(keyFileLocation)); + } +} diff --git a/common/src/main/java/org/geysermc/common/window/response/FormResponseData.java b/common/src/main/java/org/geysermc/floodgate/crypto/Topping.java similarity index 84% rename from common/src/main/java/org/geysermc/common/window/response/FormResponseData.java rename to common/src/main/java/org/geysermc/floodgate/crypto/Topping.java index 013c49859..689274269 100644 --- a/common/src/main/java/org/geysermc/common/window/response/FormResponseData.java +++ b/common/src/main/java/org/geysermc/floodgate/crypto/Topping.java @@ -23,15 +23,9 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window.response; +package org.geysermc.floodgate.crypto; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public class FormResponseData { - - private int elementID; - private String elementContent; +public interface Topping { + byte[] encode(byte[] data); + byte[] decode(byte[] data); } diff --git a/common/src/main/java/org/geysermc/floodgate/news/NewsItem.java b/common/src/main/java/org/geysermc/floodgate/news/NewsItem.java new file mode 100644 index 000000000..2f6bb4852 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/news/NewsItem.java @@ -0,0 +1,139 @@ +/* + * 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.news; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.geysermc.floodgate.news.data.ItemData; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public final class NewsItem { + private final int id; + private final String project; + private final boolean active; + private final NewsType type; + private final ItemData data; + private final boolean priority; + private final String message; + private final Set actions; + private final String url; + + private NewsItem(int id, String project, boolean active, NewsType type, ItemData data, + boolean priority, String message, Set actions, String url) { + this.id = id; + this.project = project; + this.active = active; + this.type = type; + this.data = data; + this.priority = priority; + this.message = message; + this.actions = Collections.unmodifiableSet(actions); + this.url = url; + } + + public static NewsItem readItem(JsonObject newsItem) { + NewsType newsType = NewsType.getByName(newsItem.get("type").getAsString()); + if (newsType == null) { + return null; + } + + JsonObject messageObject = newsItem.getAsJsonObject("message"); + NewsItemMessage itemMessage = NewsItemMessage.getById(messageObject.get("id").getAsInt()); + + String message = "Received an unknown news message type. Please update"; + if (itemMessage != null) { + message = itemMessage.getFormattedMessage(messageObject.getAsJsonArray("args")); + } + + Set actions = new HashSet<>(); + for (JsonElement actionElement : newsItem.getAsJsonArray("actions")) { + NewsItemAction action = NewsItemAction.getByName(actionElement.getAsString()); + if (action != null) { + actions.add(action); + } + } + + return new NewsItem( + newsItem.get("id").getAsInt(), + newsItem.get("project").getAsString(), + newsItem.get("active").getAsBoolean(), + newsType, + newsType.read(newsItem.getAsJsonObject("data")), + newsItem.get("priority").getAsBoolean(), + message, + actions, + newsItem.get("url").getAsString() + ); + } + + public int getId() { + return id; + } + + public String getProject() { + return project; + } + + public boolean isActive() { + return active; + } + + public NewsType getType() { + return type; + } + + public ItemData getData() { + return data; + } + + @SuppressWarnings("unchecked") + public T getDataAs(Class type) { + return (T) data; + } + + public boolean isPriority() { + return priority; + } + + public String getRawMessage() { + return message; + } + + public String getMessage() { + return message + " See " + getUrl() + " for more information."; + } + + public Set getActions() { + return actions; + } + + public String getUrl() { + return url; + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/news/NewsItemAction.java b/common/src/main/java/org/geysermc/floodgate/news/NewsItemAction.java new file mode 100644 index 000000000..78a8e4ed3 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/news/NewsItemAction.java @@ -0,0 +1,44 @@ +/* + * 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.news; + +public enum NewsItemAction { + ON_SERVER_STARTED, + ON_OPERATOR_JOIN, + BROADCAST_TO_CONSOLE, + BROADCAST_TO_OPERATORS; + + private static final NewsItemAction[] VALUES = values(); + + public static NewsItemAction getByName(String actionName) { + for (NewsItemAction type : VALUES) { + if (type.name().equalsIgnoreCase(actionName)) { + return type; + } + } + return null; + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/news/NewsItemMessage.java b/common/src/main/java/org/geysermc/floodgate/news/NewsItemMessage.java new file mode 100644 index 000000000..b11605fb4 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/news/NewsItemMessage.java @@ -0,0 +1,89 @@ +/* + * 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.news; + +import com.google.gson.JsonArray; + +// {} is used for things that have to be filled in by the server, +// {@} is for things that have to be filled in by us +public enum NewsItemMessage { + UPDATE_AVAILABLE("There is an update available for {}. The newest version is: {}"), + UPDATE_RECOMMENDED(UPDATE_AVAILABLE + ". Your version is quite old, updating is recommend."), + UPDATE_HIGHLY_RECOMMENDED(UPDATE_AVAILABLE + ". We highly recommend updating because some important changes have been made."), + UPDATE_ANCIENT_VERSION(UPDATE_AVAILABLE + ". You are running an ancient version, updating is recommended."), + + DOWNTIME_GENERIC("The {} is temporarily going down for maintenance soon."), + DOWNTIME_WITH_START("The {} is temporarily going down for maintenance on {}."), + DOWNTIME_TIMEFRAME(DOWNTIME_WITH_START + " The maintenance is expected to last till {}."); + + private static final NewsItemMessage[] VALUES = values(); + + private final String messageFormat; + private final String[] messageSplitted; + + NewsItemMessage(String messageFormat) { + this.messageFormat = messageFormat; + this.messageSplitted = messageFormat.split(" "); + } + + public static NewsItemMessage getById(int id) { + return VALUES.length > id ? VALUES[id] : null; + } + + public String getMessageFormat() { + return messageFormat; + } + + public String getFormattedMessage(JsonArray serverArguments) { + int serverArgumentsIndex = 0; + + StringBuilder message = new StringBuilder(); + for (String split : messageSplitted) { + if (message.length() > 0) { + message.append(' '); + } + + String result = split; + + if (serverArgumentsIndex < serverArguments.size()) { + String argument = serverArguments.get(serverArgumentsIndex).getAsString(); + result = result.replace("{}", argument); + if (!result.equals(split)) { + serverArgumentsIndex++; + } + } + + message.append(result); + } + return message.toString(); + } + + + @Override + public String toString() { + return getMessageFormat(); + } +} diff --git a/common/src/main/java/org/geysermc/common/window/button/FormImage.java b/common/src/main/java/org/geysermc/floodgate/news/NewsType.java similarity index 57% rename from common/src/main/java/org/geysermc/common/window/button/FormImage.java rename to common/src/main/java/org/geysermc/floodgate/news/NewsType.java index 034278291..c2848535e 100644 --- a/common/src/main/java/org/geysermc/common/window/button/FormImage.java +++ b/common/src/main/java/org/geysermc/floodgate/news/NewsType.java @@ -23,40 +23,37 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window.button; +package org.geysermc.floodgate.news; -import lombok.Getter; -import lombok.Setter; +import com.google.gson.JsonObject; +import org.geysermc.floodgate.news.data.BuildSpecificData; +import org.geysermc.floodgate.news.data.CheckAfterData; +import org.geysermc.floodgate.news.data.ItemData; -public class FormImage { +import java.util.function.Function; - @Getter - @Setter - private String type; +public enum NewsType { + BUILD_SPECIFIC(BuildSpecificData::read), + CHECK_AFTER(CheckAfterData::read); - @Getter - @Setter - private String data; + private static final NewsType[] VALUES = values(); - public FormImage(FormImageType type, String data) { - this.type = type.getName(); - this.data = data; + private final Function readFunction; + + NewsType(Function readFunction) { + this.readFunction = readFunction; } - public enum FormImageType { - PATH("path"), - URL("url"); - - @Getter - private String name; - - FormImageType(String name) { - this.name = name; + public static NewsType getByName(String newsType) { + for (NewsType type : VALUES) { + if (type.name().equalsIgnoreCase(newsType)) { + return type; + } } + return null; + } - @Override - public String toString() { - return name; - } + public ItemData read(JsonObject data) { + return readFunction.apply(data); } } diff --git a/common/src/main/java/org/geysermc/common/window/CustomFormBuilder.java b/common/src/main/java/org/geysermc/floodgate/news/data/BuildSpecificData.java similarity index 51% rename from common/src/main/java/org/geysermc/common/window/CustomFormBuilder.java rename to common/src/main/java/org/geysermc/floodgate/news/data/BuildSpecificData.java index f4e597f00..d415f4200 100644 --- a/common/src/main/java/org/geysermc/common/window/CustomFormBuilder.java +++ b/common/src/main/java/org/geysermc/floodgate/news/data/BuildSpecificData.java @@ -23,49 +23,38 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window; +package org.geysermc.floodgate.news.data; -import lombok.Getter; -import org.geysermc.common.window.CustomFormWindow; -import org.geysermc.common.window.button.FormImage; -import org.geysermc.common.window.component.FormComponent; -import org.geysermc.common.window.response.CustomFormResponse; +import com.google.gson.JsonObject; -public class CustomFormBuilder { +public final class BuildSpecificData implements ItemData { + private String branch; - @Getter - private CustomFormWindow form; + private boolean allAffected; + private int affectedGreaterThan; + private int affectedLessThan; - public CustomFormBuilder(String title) { - form = new CustomFormWindow(title); + public static BuildSpecificData read(JsonObject data) { + BuildSpecificData updateData = new BuildSpecificData(); + updateData.branch = data.get("branch").getAsString(); + + JsonObject affectedBuilds = data.getAsJsonObject("affected_builds"); + if (affectedBuilds.has("all")) { + updateData.allAffected = affectedBuilds.get("all").getAsBoolean(); + } + if (!updateData.allAffected) { + updateData.affectedGreaterThan = affectedBuilds.get("gt").getAsInt(); + updateData.affectedLessThan = affectedBuilds.get("lt").getAsInt(); + } + return updateData; } - public CustomFormBuilder setTitle(String title) { - form.setTitle(title); - return this; + public boolean isAffected(String branch, int buildId) { + return this.branch.equals(branch) && + (allAffected || buildId > affectedGreaterThan && buildId < affectedLessThan); } - public CustomFormBuilder setIcon(FormImage icon) { - form.setIcon(icon); - return this; - } - - public CustomFormBuilder setResponse(String data) { - form.setResponse(data); - return this; - } - - public CustomFormBuilder setResponse(CustomFormResponse response) { - form.setResponse(response); - return this; - } - - public CustomFormBuilder addComponent(FormComponent component) { - form.addComponent(component); - return this; - } - - public CustomFormWindow build() { - return form; + public String getBranch() { + return branch; } } diff --git a/common/src/main/java/org/geysermc/floodgate/news/data/CheckAfterData.java b/common/src/main/java/org/geysermc/floodgate/news/data/CheckAfterData.java new file mode 100644 index 000000000..92d01689b --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/news/data/CheckAfterData.java @@ -0,0 +1,42 @@ +/* + * 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.news.data; + +import com.google.gson.JsonObject; + +public class CheckAfterData implements ItemData { + private long checkAfter; + + public static CheckAfterData read(JsonObject data) { + CheckAfterData checkAfterData = new CheckAfterData(); + checkAfterData.checkAfter = data.get("check_after").getAsLong(); + return checkAfterData; + } + + public long getCheckAfter() { + return checkAfter; + } +} diff --git a/common/src/main/java/org/geysermc/common/window/response/FormResponse.java b/common/src/main/java/org/geysermc/floodgate/news/data/ItemData.java similarity index 94% rename from common/src/main/java/org/geysermc/common/window/response/FormResponse.java rename to common/src/main/java/org/geysermc/floodgate/news/data/ItemData.java index 35023f167..122ee775d 100644 --- a/common/src/main/java/org/geysermc/common/window/response/FormResponse.java +++ b/common/src/main/java/org/geysermc/floodgate/news/data/ItemData.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window.response; +package org.geysermc.floodgate.news.data; -public interface FormResponse { +public interface ItemData { } diff --git a/common/src/main/java/org/geysermc/floodgate/time/SntpClientUtils.java b/common/src/main/java/org/geysermc/floodgate/time/SntpClientUtils.java new file mode 100644 index 000000000..9e7f2c1b0 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/time/SntpClientUtils.java @@ -0,0 +1,91 @@ +/* + * 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.time; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.ByteBuffer; + +/* + * Thanks: + * https://datatracker.ietf.org/doc/html/rfc1769 + * https://github.com/jonsagara/SimpleNtpClient + * https://stackoverflow.com/a/29138806 + */ +public final class SntpClientUtils { + private static final int NTP_PORT = 123; + + private static final int NTP_PACKET_SIZE = 48; + private static final int NTP_MODE = 3; // client + private static final int NTP_VERSION = 3; + private static final int RECEIVE_TIME_POSITION = 32; + + private static final long NTP_TIME_OFFSET = ((365L * 70L) + 17L) * 24L * 60L * 60L; + + public static long requestTimeOffset(String host, int timeout) { + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(timeout); + + InetAddress address = InetAddress.getByName(host); + + ByteBuffer buff = ByteBuffer.allocate(NTP_PACKET_SIZE); + + DatagramPacket request = new DatagramPacket( + buff.array(), NTP_PACKET_SIZE, address, NTP_PORT + ); + + // mode is in the least signification 3 bits + // version is in bits 3-5 + buff.put((byte) (NTP_MODE | (NTP_VERSION << 3))); + + long originateTime = System.currentTimeMillis(); + socket.send(request); + + DatagramPacket response = new DatagramPacket(buff.array(), NTP_PACKET_SIZE); + socket.receive(response); + + long responseTime = System.currentTimeMillis(); + + // everything before isn't important for us + buff.position(RECEIVE_TIME_POSITION); + + long receiveTime = readTimestamp(buff); + long transmitTime = readTimestamp(buff); + + return ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2; + } catch (Exception ignored) { + } + return Long.MIN_VALUE; + } + + private static long readTimestamp(ByteBuffer buffer) { + //todo look into the ntp 2036 problem + long seconds = buffer.getInt() & 0xffffffffL; + long fraction = buffer.getInt() & 0xffffffffL; + return ((seconds - NTP_TIME_OFFSET) * 1000) + ((fraction * 1000) / 0x100000000L); + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/time/TimeSyncer.java b/common/src/main/java/org/geysermc/floodgate/time/TimeSyncer.java new file mode 100644 index 000000000..3fe089e06 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/time/TimeSyncer.java @@ -0,0 +1,67 @@ +/* + * 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.time; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public final class TimeSyncer { + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + private long timeOffset = Long.MIN_VALUE; // value when it failed to get the offset + + public TimeSyncer(String timeServer) { + executorService.scheduleWithFixedDelay(() -> { + // 5 tries to get the time offset, since UDP doesn't guaranty a response + for (int i = 0; i < 5; i++) { + long offset = SntpClientUtils.requestTimeOffset(timeServer, 3000); + if (offset != Long.MIN_VALUE) { + timeOffset = offset; + return; + } + } + }, 0, 30, TimeUnit.MINUTES); + } + + public void shutdown() { + executorService.shutdown(); + } + + public long getTimeOffset() { + return timeOffset; + } + + public long getRealMillis() { + if (hasUsefulOffset()) { + return System.currentTimeMillis() + getTimeOffset(); + } + return System.currentTimeMillis(); + } + + public boolean hasUsefulOffset() { + return timeOffset != Long.MIN_VALUE; + } +} diff --git a/common/src/main/java/org/geysermc/common/window/component/LabelComponent.java b/common/src/main/java/org/geysermc/floodgate/util/Base64Utils.java similarity index 80% rename from common/src/main/java/org/geysermc/common/window/component/LabelComponent.java rename to common/src/main/java/org/geysermc/floodgate/util/Base64Utils.java index 4608282fc..326fa2590 100644 --- a/common/src/main/java/org/geysermc/common/window/component/LabelComponent.java +++ b/common/src/main/java/org/geysermc/floodgate/util/Base64Utils.java @@ -23,20 +23,13 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window.component; +package org.geysermc.floodgate.util; -import lombok.Getter; -import lombok.Setter; - -public class LabelComponent extends FormComponent { - - @Getter - @Setter - private String text; - - public LabelComponent(String text) { - super("label"); - - this.text = text; +public final class Base64Utils { + public static int getEncodedLength(int length) { + if (length <= 0) { + return -1; + } + return 4 * ((length + 2) / 3); } } 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 22c33f475..4f4325a9b 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java +++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java @@ -25,47 +25,91 @@ package org.geysermc.floodgate.util; -import lombok.AllArgsConstructor; +import lombok.AccessLevel; import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.geysermc.floodgate.time.TimeSyncer; -import java.util.UUID; - -@AllArgsConstructor +/** + * This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. This + * class is only used internally, and you should look at FloodgatePlayer instead (FloodgatePlayer is + * present in the API module of the Floodgate repo) + */ @Getter -public class BedrockData { - public static final int EXPECTED_LENGTH = 7; - public static final String FLOODGATE_IDENTIFIER = "Geyser-Floodgate"; +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class BedrockData implements Cloneable { + public static final int EXPECTED_LENGTH = 13; - private String version; - private String username; - private String xuid; - private int deviceId; - private String languageCode; - private int inputMode; - private String ip; - private int dataLength; + private final String version; + private final String username; + private final String xuid; + private final int deviceOs; + private final String languageCode; + private final int uiProfile; + private final int inputMode; + private final String ip; + private final LinkedPlayer linkedPlayer; + private final boolean fromProxy; - public BedrockData(String version, String username, String xuid, int deviceId, String languageCode, int inputMode, String ip) { - this(version, username, xuid, deviceId, languageCode, inputMode, ip, EXPECTED_LENGTH); + private final int subscribeId; + private final String verifyCode; + + private final long timestamp; + 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, int subscribeId, + String verifyCode, TimeSyncer timeSyncer) { + return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode, + uiProfile, ip, linkedPlayer, fromProxy, subscribeId, verifyCode, + timeSyncer.getRealMillis(), EXPECTED_LENGTH); + } + + public static BedrockData of( + String version, String username, String xuid, int deviceOs, + String languageCode, int uiProfile, int inputMode, String ip, + int subscribeId, String verifyCode, TimeSyncer timeSyncer) { + return of(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null, + false, subscribeId, verifyCode, timeSyncer); } public static BedrockData fromString(String data) { String[] split = data.split("\0"); - if (split.length != EXPECTED_LENGTH) return null; + if (split.length != EXPECTED_LENGTH) { + return emptyData(split.length); + } + LinkedPlayer linkedPlayer = LinkedPlayer.fromString(split[8]); + // 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]), split[6], split.length + 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[9]), Integer.parseInt(split[10]), split[11], Long.parseLong(split[12]), split.length ); } - public static BedrockData fromRawData(byte[] data) { - return fromString(new String(data)); + private static BedrockData emptyData(int dataLength) { + return new BedrockData(null, null, null, -1, null, -1, -1, null, null, false, -1, null, -1, + dataLength); + } + + public boolean hasPlayerLink() { + return linkedPlayer != null; } @Override public String toString() { - return version +'\0'+ username +'\0'+ xuid +'\0'+ deviceId +'\0'+ languageCode +'\0'+ - inputMode +'\0'+ ip; + // The format is the same as the order of the fields in this class + return version + '\0' + username + '\0' + xuid + '\0' + deviceOs + '\0' + + languageCode + '\0' + uiProfile + '\0' + inputMode + '\0' + ip + '\0' + + (linkedPlayer != null ? linkedPlayer.toString() : "null") + '\0' + + (fromProxy ? 1 : 0) + '\0' + subscribeId + '\0' + verifyCode + '\0' + timestamp; + } + + @Override + public BedrockData clone() throws CloneNotSupportedException { + return (BedrockData) super.clone(); } } diff --git a/common/src/main/java/org/geysermc/floodgate/util/DeviceOS.java b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java similarity index 70% rename from common/src/main/java/org/geysermc/floodgate/util/DeviceOS.java rename to common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java index 1e0b22cf6..714af6f1f 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/DeviceOS.java +++ b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java @@ -25,36 +25,41 @@ package org.geysermc.floodgate.util; -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; -public enum DeviceOS { - - @JsonEnumDefaultValue +/** + * The Operation Systems where Bedrock players can connect with + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum DeviceOs { UNKNOWN("Unknown"), - ANDROID("Android"), + GOOGLE("Android"), IOS("iOS"), OSX("macOS"), - FIREOS("FireOS"), + AMAZON("Amazon"), GEARVR("Gear VR"), HOLOLENS("Hololens"), - WIN10("Windows 10"), - WIN32("Windows"), + UWP("Windows 10"), + WIN32("Windows x86"), DEDICATED("Dedicated"), - ORBIS("PS4"), + TVOS("Apple TV"), + PS4("PS4"), NX("Switch"), - SWITCH("Switch"), - XBOX_ONE("Xbox One"), - WIN_PHONE("Windows Phone"); + XBOX("Xbox One"), + WINDOWS_PHONE("Windows Phone"); - private static final DeviceOS[] VALUES = values(); + private static final DeviceOs[] VALUES = values(); private final String displayName; - DeviceOS(final String displayName) { - this.displayName = displayName; - } - - public static DeviceOS getById(int id) { + /** + * Get the DeviceOs instance from the identifier. + * + * @param id the DeviceOs identifier + * @return The DeviceOs or {@link #UNKNOWN} if the DeviceOs wasn't found + */ + public static DeviceOs getById(int id) { return id < VALUES.length ? VALUES[id] : VALUES[0]; } diff --git a/common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java b/common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java deleted file mode 100644 index f4dd5d2e3..000000000 --- a/common/src/main/java/org/geysermc/floodgate/util/EncryptionUtil.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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; - -import javax.crypto.*; -import javax.crypto.spec.SecretKeySpec; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.*; -import java.security.spec.EncodedKeySpec; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; - -public class EncryptionUtil { - public static String encrypt(Key key, String data) throws IllegalBlockSizeException, - InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { - KeyGenerator generator = KeyGenerator.getInstance("AES"); - generator.init(128); - SecretKey secretKey = generator.generateKey(); - - Cipher cipher = Cipher.getInstance("AES"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] encryptedText = cipher.doFinal(data.getBytes()); - - cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(key instanceof PublicKey ? Cipher.PUBLIC_KEY : Cipher.PRIVATE_KEY, key); - return Base64.getEncoder().encodeToString(cipher.doFinal(secretKey.getEncoded())) + '\0' + - Base64.getEncoder().encodeToString(encryptedText); - } - - public static String encryptBedrockData(Key key, BedrockData data) throws IllegalBlockSizeException, - InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { - return encrypt(key, data.toString()); - } - - public static byte[] decrypt(Key key, String encryptedData) throws IllegalBlockSizeException, - InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { - String[] split = encryptedData.split("\0"); - if (split.length != 2) { - throw new IllegalArgumentException("Expected two arguments, got " + split.length); - } - - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(key instanceof PublicKey ? Cipher.PUBLIC_KEY : Cipher.PRIVATE_KEY, key); - byte[] decryptedKey = cipher.doFinal(Base64.getDecoder().decode(split[0])); - - SecretKey secretKey = new SecretKeySpec(decryptedKey, 0, decryptedKey.length, "AES"); - cipher = Cipher.getInstance("AES"); - cipher.init(Cipher.DECRYPT_MODE, secretKey); - return cipher.doFinal(Base64.getDecoder().decode(split[1])); - } - - public static BedrockData decryptBedrockData(Key key, String encryptedData) throws IllegalBlockSizeException, - InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { - return BedrockData.fromRawData(decrypt(key, encryptedData)); - } - - @SuppressWarnings("unchecked") - public static T getKeyFromFile(Path fileLocation, Class keyType) throws - IOException, InvalidKeySpecException, NoSuchAlgorithmException { - boolean isPublicKey = keyType == PublicKey.class; - if (!isPublicKey && keyType != PrivateKey.class) { - throw new RuntimeException("I can only read public and private keys!"); - } - - byte[] key = Files.readAllBytes(fileLocation); - - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - EncodedKeySpec keySpec = isPublicKey ? new X509EncodedKeySpec(key) : new PKCS8EncodedKeySpec(key); - return (T) (isPublicKey ? - keyFactory.generatePublic(keySpec) : - keyFactory.generatePrivate(keySpec) - ); - } -} diff --git a/common/src/main/java/org/geysermc/common/window/component/FormComponent.java b/common/src/main/java/org/geysermc/floodgate/util/FloodgateConfigHolder.java similarity index 86% rename from common/src/main/java/org/geysermc/common/window/component/FormComponent.java rename to common/src/main/java/org/geysermc/floodgate/util/FloodgateConfigHolder.java index 6b503eb49..d33840bb4 100644 --- a/common/src/main/java/org/geysermc/common/window/component/FormComponent.java +++ b/common/src/main/java/org/geysermc/floodgate/util/FloodgateConfigHolder.java @@ -23,16 +23,13 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window.component; +package org.geysermc.floodgate.util; import lombok.Getter; +import lombok.Setter; -public abstract class FormComponent { - +public final class FloodgateConfigHolder { @Getter - private final String type; - - public FormComponent(String type) { - this.type = type; - } + @Setter + private static Object config; } diff --git a/common/src/main/java/org/geysermc/floodgate/util/InputMode.java b/common/src/main/java/org/geysermc/floodgate/util/InputMode.java new file mode 100644 index 000000000..d49d2ea84 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/InputMode.java @@ -0,0 +1,47 @@ +/* + * 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; + +public enum InputMode { + UNKNOWN, + KEYBOARD_MOUSE, + TOUCH, + CONTROLLER, + VR; + + private static final InputMode[] VALUES = values(); + + /** + * Get the InputMode instance from the identifier. + * + * @param id the InputMode identifier + * @return The InputMode or {@link #UNKNOWN} if the DeviceOs wasn't found + */ + public static InputMode getById(int id) { + return VALUES.length > id ? VALUES[id] : VALUES[0]; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java b/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java new file mode 100644 index 000000000..9ec5b1710 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java @@ -0,0 +1,51 @@ +/* + * 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.Getter; + +@Getter +public class InvalidFormatException extends Exception { + private boolean header = false; + + public InvalidFormatException() { + super(); + } + + public InvalidFormatException(String message) { + super(message); + } + + public InvalidFormatException(String message, boolean header) { + super(message); + this.header = header; + } + + public InvalidFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java new file mode 100644 index 000000000..f91bfafbc --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java @@ -0,0 +1,82 @@ +/* + * 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.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.UUID; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class LinkedPlayer implements Cloneable { + /** + * The Java username of the linked player + */ + private final String javaUsername; + /** + * The Java UUID of the linked player + */ + private final UUID javaUniqueId; + /** + * The UUID of the Bedrock player + */ + private final UUID bedrockId; + /** + * If the LinkedPlayer is sent from a different platform. For example the LinkedPlayer is from + * Bungee but the data has been sent to the Bukkit server. + */ + private boolean fromDifferentPlatform = false; + + public static LinkedPlayer of(String javaUsername, UUID javaUniqueId, UUID bedrockId) { + return new LinkedPlayer(javaUsername, javaUniqueId, bedrockId); + } + + public static LinkedPlayer fromString(String data) { + String[] split = data.split(";"); + if (split.length != 3) { + return null; + } + + LinkedPlayer player = new LinkedPlayer( + split[0], UUID.fromString(split[1]), UUID.fromString(split[2]) + ); + player.fromDifferentPlatform = true; + return player; + } + + @Override + public String toString() { + return javaUsername + ';' + javaUniqueId.toString() + ';' + bedrockId.toString(); + } + + @Override + public LinkedPlayer clone() throws CloneNotSupportedException { + return (LinkedPlayer) super.clone(); + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java b/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java new file mode 100644 index 000000000..af1121f3c --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java @@ -0,0 +1,44 @@ +/* + * 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; + +public enum UiProfile { + CLASSIC, + POCKET; + + private static final UiProfile[] VALUES = values(); + + /** + * Get the UiProfile instance from the identifier. + * + * @param id the UiProfile identifier + * @return The UiProfile or {@link #CLASSIC} if the UiProfile wasn't found + */ + public static UiProfile getById(int id) { + return VALUES.length > id ? VALUES[id] : VALUES[0]; + } +} 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..f24dc8145 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java @@ -0,0 +1,91 @@ +/* + * 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 { + /** + * Sent once we successfully connected to the server + */ + SUBSCRIBER_CREATED(0), + /** + * Sent every time a subscriber got added or disconnected + */ + SUBSCRIBER_COUNT(1), + /** + * Sent once the creator disconnected. After this packet the server will automatically close the + * connection once the queue size (sent in {@link #ADDED_TO_QUEUE} and {@link #SKIN_UPLOADED} + * reaches 0. + */ + CREATOR_DISCONNECTED(4), + + /** + * Sent every time a skin got added to the upload queue + */ + ADDED_TO_QUEUE(2), + /** + * Sent every time a skin got successfully uploaded + */ + SKIN_UPLOADED(3), + + /** + * Sent every time a news item was added + */ + NEWS_ADDED(6), + + /** + * Sent when the server wants you to know something. Currently used for violations that aren't + * bad enough to close the connection + */ + LOG_MESSAGE(5); + + private static final WebsocketEventType[] VALUES; + + static { + WebsocketEventType[] values = values(); + VALUES = new WebsocketEventType[values.length]; + for (WebsocketEventType value : values) { + VALUES[value.id] = value; + } + } + + /** + * The ID is based of the time it got added. However, to keep the enum organized as time goes on, + * it looks nicer to sort the events based of categories. + */ + private final int id; + + WebsocketEventType(int id) { + this.id = id; + } + + public static WebsocketEventType getById(int id) { + return VALUES.length > id ? VALUES[id] : null; + } + + public int getId() { + return id; + } +} diff --git a/connector/pom.xml b/connector/pom.xml index 742e792d8..6701225ae 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -6,7 +6,7 @@ org.geysermc geyser-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT connector @@ -20,7 +20,13 @@ org.geysermc common - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT + compile + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.10.2 compile @@ -29,6 +35,12 @@ 2.10.2 compile + + org.java-websocket + Java-WebSocket + 1.5.1 + compile + com.github.CloudburstMC.Protocol bedrock-v440 @@ -206,11 +218,13 @@ org.reflections reflections 0.9.11 + compile org.dom4j dom4j 2.1.3 + compile net.kyori @@ -246,6 +260,7 @@ com.github.GeyserMC MCAuthLib 0e48a094f2 + compile diff --git a/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java b/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java index 77492fb7a..7f5bca8c2 100644 --- a/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java +++ b/connector/src/main/java/org/geysermc/connector/FloodgateKeyLoader.java @@ -33,11 +33,20 @@ import java.nio.file.Path; public class FloodgateKeyLoader { public static Path getKeyPath(GeyserJacksonConfiguration config, Object floodgate, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) { + if (!config.getRemote().getAuthType().equals("floodgate")) { + return geyserDataFolder.resolve(config.getFloodgateKeyFile()); + } + Path floodgateKey = geyserDataFolder.resolve(config.getFloodgateKeyFile()); - if (!Files.exists(floodgateKey) && config.getRemote().getAuthType().equals("floodgate")) { + if (config.getFloodgateKeyFile().equals("public-key.pem")) { + logger.info("Floodgate 2.0 doesn't use a public/private key system anymore. We'll search for key.pem instead"); + floodgateKey = geyserDataFolder.resolve("key.pem"); + } + + if (!Files.exists(floodgateKey)) { if (floodgate != null) { - Path autoKey = floodgateDataFolder.resolve("public-key.pem"); + Path autoKey = floodgateDataFolder.resolve("key.pem"); if (Files.exists(autoKey)) { logger.info(LanguageUtils.getLocaleStringLog("geyser.bootstrap.floodgate.auto_loaded")); floodgateKey = autoKey; diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index 89e7a06d7..34dd0b849 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -58,7 +58,14 @@ 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.*; +import org.geysermc.floodgate.crypto.AesCipher; +import org.geysermc.floodgate.crypto.AesKeyProducer; +import org.geysermc.floodgate.crypto.Base64Topping; +import org.geysermc.floodgate.crypto.FloodgateCipher; +import org.geysermc.floodgate.news.NewsItemAction; +import org.geysermc.floodgate.time.TimeSyncer; import org.jetbrains.annotations.Contract; import javax.naming.directory.Attribute; @@ -66,6 +73,7 @@ import javax.naming.directory.InitialDirContext; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.security.Key; import java.text.DecimalFormat; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -75,7 +83,6 @@ import java.util.concurrent.TimeUnit; @Getter public class GeyserConnector { - public static final ObjectMapper JSON_MAPPER = new ObjectMapper() .enable(JsonParser.Feature.IGNORE_UNDEFINED) .enable(JsonParser.Feature.ALLOW_COMMENTS) @@ -102,6 +109,11 @@ public class GeyserConnector { @Setter private AuthType defaultAuthType; + private final TimeSyncer timeSyncer; + private FloodgateCipher cipher; + private FloodgateSkinUploader skinUploader; + private final NewsHandler newsHandler; + private boolean shuttingDown = false; private final ScheduledExecutorService generalThreadPool; @@ -190,6 +202,36 @@ public class GeyserConnector { defaultAuthType = AuthType.getByName(config.getRemote().getAuthType()); + TimeSyncer timeSyncer = null; + if (defaultAuthType == AuthType.FLOODGATE) { + timeSyncer = new TimeSyncer(Constants.NTP_SERVER); + try { + Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath()); + 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); + } + } + this.timeSyncer = timeSyncer; + + String branch = "unknown"; + int buildNumber = -1; + try { + Properties gitProperties = new Properties(); + gitProperties.load(FileUtils.getResource("git.properties")); + branch = gitProperties.getProperty("git.branch"); + String build = gitProperties.getProperty("git.build.number"); + if (build != null) { + buildNumber = Integer.parseInt(build); + } + } catch (Exception e) { + logger.error("Failed to read git.properties", e); + } + newsHandler = new NewsHandler(branch, buildNumber); + CooldownUtils.setDefaultShowCooldown(config.getShowCooldown()); DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether SkullBlockEntityTranslator.ALLOW_CUSTOM_SKULLS = config.isAllowCustomSkulls(); @@ -241,7 +283,7 @@ public class GeyserConnector { for (GeyserSession session : players) { if (session == null) continue; if (session.getClientData() == null) continue; - String os = session.getClientData().getDeviceOS().toString(); + String os = session.getClientData().getDeviceOs().toString(); if (!valueMap.containsKey(os)) { valueMap.put(os, 1); } else { @@ -303,6 +345,8 @@ public class GeyserConnector { if (platformType == PlatformType.STANDALONE) { logger.warning(LanguageUtils.getLocaleStringLog("geyser.core.movement_warn")); } + + newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED); } public void shutdown() { @@ -347,6 +391,8 @@ public class GeyserConnector { generalThreadPool.shutdown(); bedrockServer.close(); + timeSyncer.shutdown(); + newsHandler.shutdown(); players.clear(); defaultAuthType = null; this.getCommandManager().getCommands().clear(); @@ -425,6 +471,10 @@ public class GeyserConnector { return bootstrap.getWorldManager(); } + public TimeSyncer getTimeSyncer() { + return timeSyncer; + } + /** * Whether to use XML reflections in the jar or manually find the reflections. * Will return true if the version number is not 'DEV' and the platform is not Fabric. diff --git a/connector/src/main/java/org/geysermc/connector/command/CommandManager.java b/connector/src/main/java/org/geysermc/connector/command/CommandManager.java index 9f675ae81..f62fd8539 100644 --- a/connector/src/main/java/org/geysermc/connector/command/CommandManager.java +++ b/connector/src/main/java/org/geysermc/connector/command/CommandManager.java @@ -53,7 +53,7 @@ public abstract class CommandManager { registerCommand(new VersionCommand(connector, "version", "geyser.commands.version.desc", "geyser.command.version")); registerCommand(new SettingsCommand(connector, "settings", "geyser.commands.settings.desc", "geyser.command.settings")); registerCommand(new StatisticsCommand(connector, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics")); - registerCommand(new AdvancementsCommand(connector, "advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); + registerCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements")); } public void registerCommand(GeyserCommand command) { diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java index 2ef23381b..8ace83840 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/AdvancementsCommand.java @@ -25,25 +25,20 @@ package org.geysermc.connector.command.defaults; -import org.geysermc.common.window.SimpleFormWindow; -import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.command.GeyserCommand; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.session.cache.AdvancementsCache; public class AdvancementsCommand extends GeyserCommand { - - public AdvancementsCommand(GeyserConnector connector, String name, String description, String permission) { + public AdvancementsCommand(String name, String description, String permission) { super(name, description, permission); } @Override public void execute(GeyserSession session, CommandSender sender, String[] args) { - if (session == null) return; - - SimpleFormWindow window = session.getAdvancementsCache().buildMenuForm(); - session.sendForm(window, AdvancementsCache.ADVANCEMENTS_MENU_FORM_ID); + if (session != null) { + session.getAdvancementsCache().buildAndShowMenuForm(); + } } @Override diff --git a/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java b/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java index 2aeee1377..0f85e3c8f 100644 --- a/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java +++ b/connector/src/main/java/org/geysermc/connector/command/defaults/SettingsCommand.java @@ -32,17 +32,15 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.SettingsUtils; public class SettingsCommand extends GeyserCommand { - public SettingsCommand(GeyserConnector connector, String name, String description, String permission) { super(name, description, permission); } @Override public void execute(GeyserSession session, CommandSender sender, String[] args) { - if (session == null) return; - - SettingsUtils.buildForm(session); - session.sendForm(session.getSettingsForm(), SettingsUtils.SETTINGS_FORM_ID); + if (session != null) { + session.sendForm(SettingsUtils.buildForm(session)); + } } @Override diff --git a/connector/src/main/java/org/geysermc/connector/dump/BootstrapDumpInfo.java b/connector/src/main/java/org/geysermc/connector/dump/BootstrapDumpInfo.java index b26d117f9..afcbda6a4 100644 --- a/connector/src/main/java/org/geysermc/connector/dump/BootstrapDumpInfo.java +++ b/connector/src/main/java/org/geysermc/connector/dump/BootstrapDumpInfo.java @@ -34,8 +34,7 @@ import java.util.List; @Getter public class BootstrapDumpInfo { - - private PlatformType platform; + private final PlatformType platform; public BootstrapDumpInfo() { this.platform = GeyserConnector.getInstance().getPlatformType(); @@ -44,7 +43,6 @@ public class BootstrapDumpInfo { @Getter @AllArgsConstructor public static class PluginInfo { - public boolean enabled; public String name; public String version; @@ -55,7 +53,6 @@ public class BootstrapDumpInfo { @Getter @AllArgsConstructor public static class ListenerInfo { - public String ip; public int port; } diff --git a/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java b/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java index d8d5d7218..f381bd510 100644 --- a/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java +++ b/connector/src/main/java/org/geysermc/connector/dump/DumpInfo.java @@ -41,7 +41,8 @@ import org.geysermc.connector.network.BedrockProtocol; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.DockerCheck; import org.geysermc.connector.utils.FileUtils; -import org.geysermc.floodgate.util.DeviceOS; +import org.geysermc.floodgate.util.DeviceOs; +import org.geysermc.floodgate.util.FloodgateConfigHolder; import java.io.File; import java.io.IOException; @@ -53,27 +54,29 @@ import java.util.Properties; @Getter public class DumpInfo { - @JsonIgnore private static final long MEGABYTE = 1024L * 1024L; private final DumpInfo.VersionInfo versionInfo; private Properties gitInfo; private final GeyserConfiguration config; + private final Object floodgateConfig; private final HashInfo hashInfo; - private final Object2IntMap userPlatforms; + private final Object2IntMap userPlatforms; private final RamInfo ramInfo; private final BootstrapDumpInfo bootstrapInfo; public DumpInfo() { - this.versionInfo = new DumpInfo.VersionInfo(); + this.versionInfo = new VersionInfo(); try { this.gitInfo = new Properties(); this.gitInfo.load(FileUtils.getResource("git.properties")); - } catch (IOException ignored) { } + } catch (IOException ignored) { + } this.config = GeyserConnector.getInstance().getConfig(); + this.floodgateConfig = FloodgateConfigHolder.getConfig(); String md5Hash = "unknown"; String sha256Hash = "unknown"; @@ -99,7 +102,7 @@ public class DumpInfo { this.userPlatforms = new Object2IntOpenHashMap<>(); for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) { - DeviceOS device = session.getClientData().getDeviceOS(); + DeviceOs device = session.getClientData().getDeviceOs(); userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1); } @@ -108,7 +111,6 @@ public class DumpInfo { @Getter public static class VersionInfo { - private final String name; private final String version; private final String javaVersion; @@ -123,7 +125,8 @@ public class DumpInfo { this.name = GeyserConnector.NAME; this.version = GeyserConnector.VERSION; this.javaVersion = System.getProperty("java.version"); - this.architecture = System.getProperty("os.arch"); // Usually gives Java architecture but still may be helpful. + // Usually gives Java architecture but still may be helpful. + this.architecture = System.getProperty("os.arch"); this.operatingSystem = System.getProperty("os.name"); this.operatingSystemVersion = System.getProperty("os.version"); @@ -141,9 +144,8 @@ public class DumpInfo { @Getter public static class NetworkInfo { - - private String internalIP; private final boolean dockerCheck; + private String internalIP; NetworkInfo() { if (AsteriskSerializer.showSensitive) { @@ -156,7 +158,8 @@ public class DumpInfo { try { // Fallback to the normal way of getting the local IP this.internalIP = InetAddress.getLocalHost().getHostAddress(); - } catch (UnknownHostException ignored) { } + } catch (UnknownHostException ignored) { + } } } else { // Sometimes the internal IP is the external IP... @@ -169,7 +172,6 @@ public class DumpInfo { @Getter public static class MCInfo { - private final String bedrockVersion; private final int bedrockProtocol; private final String javaVersion; @@ -185,7 +187,6 @@ public class DumpInfo { @Getter public static class RamInfo { - private final long free; private final long total; private final long max; diff --git a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java index 6ae0648b7..ad29c4b3f 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/FireworkEntity.java @@ -41,7 +41,7 @@ import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.FireworkColor; import org.geysermc.connector.utils.MathUtils; -import org.geysermc.floodgate.util.DeviceOS; +import org.geysermc.floodgate.util.DeviceOs; import java.util.ArrayList; import java.util.List; @@ -68,7 +68,8 @@ public class FireworkEntity extends Entity { // TODO: Remove once Mojang fixes bugs with fireworks crashing clients on these specific devices. // https://bugs.mojang.com/browse/MCPE-89115 - if (session.getClientData().getDeviceOS() == DeviceOS.XBOX_ONE || session.getClientData().getDeviceOS() == DeviceOS.ORBIS) { + if (session.getClientData().getDeviceOs() == DeviceOs.XBOX + || session.getClientData().getDeviceOs() == DeviceOs.PS4) { return; } diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java index de5526d52..be34ca81b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java +++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java @@ -34,7 +34,6 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.common.AuthType; import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.session.cache.AdvancementsCache; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator1_17_0; @@ -150,22 +149,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { @Override public boolean handle(ModalFormResponsePacket packet) { - switch (packet.getFormId()) { - case AdvancementsCache.ADVANCEMENT_INFO_FORM_ID: - return session.getAdvancementsCache().handleInfoForm(packet.getFormData()); - case AdvancementsCache.ADVANCEMENTS_LIST_FORM_ID: - return session.getAdvancementsCache().handleListForm(packet.getFormData()); - case AdvancementsCache.ADVANCEMENTS_MENU_FORM_ID: - return session.getAdvancementsCache().handleMenuForm(packet.getFormData()); - case SettingsUtils.SETTINGS_FORM_ID: - return SettingsUtils.handleSettingsForm(session, packet.getFormData()); - case StatisticsUtils.STATISTICS_LIST_FORM_ID: - return StatisticsUtils.handleListForm(session, packet.getFormData()); - case StatisticsUtils.STATISTICS_MENU_FORM_ID: - return StatisticsUtils.handleMenuForm(session, packet.getFormData()); - } - - return LoginEncryptionUtils.authenticateFromForm(session, connector, packet.getFormId(), packet.getFormData()); + session.getFormCache().handleResponse(packet); + return true; } private boolean couldLoginUserByName(String bedrockUsername) { @@ -193,7 +178,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { if (!session.isLoggedIn() && !session.isLoggingIn() && session.getRemoteAuthType() == AuthType.ONLINE) { // TODO it is safer to key authentication on something that won't change (UUID, not username) if (!couldLoginUserByName(session.getAuthData().getName())) { - LoginEncryptionUtils.showLoginWindow(session); + LoginEncryptionUtils.buildAndShowLoginWindow(session); } // else we were able to log the user in } 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 346fc18f5..90b5e5929 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 @@ -70,8 +70,6 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; import lombok.Setter; -import org.geysermc.common.window.CustomFormWindow; -import org.geysermc.common.window.FormWindow; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.command.CommandSender; import org.geysermc.connector.common.AuthType; @@ -96,18 +94,17 @@ import org.geysermc.connector.network.translators.collision.CollisionManager; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.connector.skin.FloodgateSkinUploader; import org.geysermc.connector.skin.SkinManager; import org.geysermc.connector.utils.*; +import org.geysermc.cumulus.Form; +import org.geysermc.cumulus.util.FormBuilder; +import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.util.BedrockData; -import org.geysermc.floodgate.util.EncryptionUtil; -import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; @@ -143,10 +140,10 @@ public class GeyserSession implements CommandSender { private ChunkCache chunkCache; private EntityCache entityCache; private EntityEffectCache effectCache; + private final FormCache formCache; private final PreferencesCache preferencesCache; private final TagCache tagCache; private WorldCache worldCache; - private WindowCache windowCache; private final Int2ObjectMap teleportMap = new Int2ObjectOpenHashMap<>(); private final PlayerInventory playerInventory; @@ -362,9 +359,6 @@ public class GeyserSession implements CommandSender { private boolean reducedDebugInfo = false; - @Setter - private CustomFormWindow settingsForm; - /** * The op permission level set by the server */ @@ -408,7 +402,7 @@ public class GeyserSession implements CommandSender { /** * Stores the last text inputted into a sign. - * + *

* Bedrock sends packets every time you update the sign, Java only wants the final packet. * Until we determine that the user has finished editing, we save the sign's current status. */ @@ -445,10 +439,10 @@ public class GeyserSession implements CommandSender { this.chunkCache = new ChunkCache(this); this.entityCache = new EntityCache(this); this.effectCache = new EntityEffectCache(); + this.formCache = new FormCache(this); this.preferencesCache = new PreferencesCache(this); this.tagCache = new TagCache(); this.worldCache = new WorldCache(this); - this.windowCache = new WindowCache(this); this.collisionManager = new CollisionManager(this); @@ -577,7 +571,16 @@ public class GeyserSession implements CommandSender { protocol = new MinecraftProtocol(authenticationService.getSelectedProfile(), authenticationService.getAccessToken()); } else { - protocol = new MinecraftProtocol(username); + // always replace spaces when using Floodgate, + // as usernames with spaces cause issues with Bungeecord's login cycle. + // However, this doesn't affect the final username as Floodgate is still in charge of that. + // So if you have (for example) replace spaces enabled on Floodgate the spaces will re-appear. + String validUsername = username; + if (remoteAuthType == AuthType.FLOODGATE) { + validUsername = username.replace(' ', '_'); + } + + protocol = new MinecraftProtocol(validUsername); } connectDownstream(); @@ -606,7 +609,7 @@ public class GeyserSession implements CommandSender { MsaAuthenticationService msaAuthenticationService = new MsaAuthenticationService(GeyserConnector.OAUTH_CLIENT_ID); MsaAuthenticationService.MsCodeResponse response = msaAuthenticationService.getAuthCode(); - LoginEncryptionUtils.showMicrosoftCodeWindow(this, response); + LoginEncryptionUtils.buildAndShowMicrosoftCodeWindow(this, response); // This just looks cool SetTimePacket packet = new SetTimePacket(); @@ -651,24 +654,6 @@ public class GeyserSession implements CommandSender { */ private void connectDownstream() { boolean floodgate = this.remoteAuthType == AuthType.FLOODGATE; - final PublicKey publicKey; - - if (floodgate) { - PublicKey key = null; - try { - key = EncryptionUtil.getKeyFromFile( - connector.getConfig().getFloodgateKeyPath(), - PublicKey.class - ); - } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException e) { - connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.bad_key"), e); - } - publicKey = key; - } else publicKey = null; - - if (publicKey != null) { - connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.loaded_key")); - } // Start ticking tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); @@ -690,22 +675,40 @@ public class GeyserSession implements CommandSender { if (event.getPacket() instanceof HandshakePacket) { String addressSuffix; if (floodgate) { - String encrypted = ""; + byte[] encryptedData; + try { - encrypted = EncryptionUtil.encryptBedrockData(publicKey, new BedrockData( + FloodgateSkinUploader skinUploader = connector.getSkinUploader(); + FloodgateCipher cipher = connector.getCipher(); + + encryptedData = cipher.encryptFromString(BedrockData.of( clientData.getGameVersion(), authData.getName(), authData.getXboxUUID(), - clientData.getDeviceOS().ordinal(), + clientData.getDeviceOs().ordinal(), clientData.getLanguageCode(), + clientData.getUiProfile().ordinal(), clientData.getCurrentInputMode().ordinal(), - upstream.getAddress().getAddress().getHostAddress() - )); + upstream.getAddress().getAddress().getHostAddress(), + skinUploader.getId(), + skinUploader.getVerifyCode(), + connector.getTimeSyncer() + ).toString()); + + if (!connector.getTimeSyncer().hasUsefulOffset()) { + connector.getLogger().warning( + "We couldn't make sure that your system clock is accurate. " + + "This can cause issues with logging in." + ); + } + } catch (Exception e) { connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e); + disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.floodgate.encryption_fail", getClientData().getLanguageCode())); + return; } - addressSuffix = '\0' + BedrockData.FLOODGATE_IDENTIFIER + '\0' + encrypted; + addressSuffix = '\0' + new String(encryptedData, StandardCharsets.UTF_8); } else { addressSuffix = ""; } @@ -788,6 +791,12 @@ public class GeyserSession implements CommandSender { if (remoteAuthType == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) { SkinManager.handleBedrockSkin(playerEntity, clientData); } + + if (remoteAuthType == AuthType.FLOODGATE) { + // 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); @@ -832,7 +841,6 @@ public class GeyserSession implements CommandSender { this.entityCache = null; this.effectCache = null; this.worldCache = null; - this.windowCache = null; closed = true; } @@ -973,10 +981,6 @@ public class GeyserSession implements CommandSender { return clientData.getLanguageCode(); } - public void sendForm(FormWindow window, int id) { - windowCache.showWindow(window, id); - } - public void setRenderDistance(int renderDistance) { renderDistance = GenericMath.ceil(++renderDistance * MathUtils.SQRT_OF_TWO); //square to circle this.renderDistance = renderDistance; @@ -990,8 +994,12 @@ public class GeyserSession implements CommandSender { return this.upstream.getAddress(); } - public void sendForm(FormWindow window) { - windowCache.showWindow(window); + public void sendForm(Form form) { + formCache.showForm(form); + } + + public void sendForm(FormBuilder formBuilder) { + formCache.showForm(formBuilder.build()); } private void startGame() { @@ -1050,7 +1058,7 @@ public class GeyserSession implements CommandSender { settings.setRewindHistorySize(0); settings.setServerAuthoritativeBlockBreaking(false); startGamePacket.setPlayerMovementSettings(settings); - + upstream.sendPacket(startGamePacket); } @@ -1168,7 +1176,7 @@ public class GeyserSession implements CommandSender { /** * Queue a packet to be sent to player. - * + * * @param packet the bedrock packet from the NukkitX protocol lib */ public void sendUpstreamPacket(BedrockPacket packet) { @@ -1233,7 +1241,7 @@ public class GeyserSession implements CommandSender { * Send a gamerule value to the client * * @param gameRule The gamerule to send - * @param value The value of the gamerule + * @param value The value of the gamerule */ public void sendGameRule(String gameRule, Object value) { GameRulesChangedPacket gameRulesChangedPacket = new GameRulesChangedPacket(); 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 16e06c066..fc4d1164a 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 @@ -25,17 +25,18 @@ package org.geysermc.connector.network.session.auth; -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; -import org.geysermc.floodgate.util.DeviceOS; +import org.geysermc.floodgate.util.DeviceOs; +import org.geysermc.floodgate.util.InputMode; +import org.geysermc.floodgate.util.UiProfile; import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) @Getter -public class BedrockClientData { +public final class BedrockClientData { @JsonProperty(value = "GameVersion") private String gameVersion; @JsonProperty(value = "ServerAddress") @@ -77,9 +78,9 @@ public class BedrockClientData { @JsonProperty(value = "DeviceModel") private String deviceModel; @JsonProperty(value = "DeviceOS") - private DeviceOS deviceOS; + private DeviceOs deviceOs; @JsonProperty(value = "UIProfile") - private UIProfile uiProfile; + private UiProfile uiProfile; @JsonProperty(value = "GuiScale") private int guiScale; @JsonProperty(value = "CurrentInputMode") @@ -106,18 +107,19 @@ public class BedrockClientData { @JsonProperty(value = "PlayFabId") private String playFabId; - public enum UIProfile { - @JsonEnumDefaultValue - CLASSIC, - POCKET + public DeviceOs getDeviceOs() { + return deviceOs != null ? deviceOs : DeviceOs.UNKNOWN; } - public enum InputMode { - @JsonEnumDefaultValue - UNKNOWN, - KEYBOARD_MOUSE, - TOUCH, // I guess Touch? - CONTROLLER, - VR + public InputMode getCurrentInputMode() { + return currentInputMode != null ? currentInputMode : InputMode.UNKNOWN; + } + + public InputMode getDefaultInputMode() { + return defaultInputMode != null ? defaultInputMode : InputMode.UNKNOWN; + } + + public UiProfile getUiProfile() { + return uiProfile != null ? uiProfile : UiProfile.CLASSIC; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java index 369967acc..98ec5b262 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/AdvancementsCache.java @@ -29,26 +29,19 @@ import com.github.steveice10.mc.protocol.data.game.advancement.Advancement; import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientAdvancementTabPacket; import lombok.Getter; import lombok.Setter; -import org.geysermc.common.window.SimpleFormWindow; -import org.geysermc.common.window.button.FormButton; -import org.geysermc.common.window.response.SimpleFormResponse; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.geysermc.connector.utils.GeyserAdvancement; import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LocaleUtils; +import org.geysermc.cumulus.SimpleForm; +import org.geysermc.cumulus.response.SimpleFormResponse; import java.util.HashMap; import java.util.List; import java.util.Map; public class AdvancementsCache { - - // Different form IDs - public static final int ADVANCEMENTS_MENU_FORM_ID = 1341; - public static final int ADVANCEMENTS_LIST_FORM_ID = 1342; - public static final int ADVANCEMENT_INFO_FORM_ID = 1343; - /** * Stores the player's advancement progress */ @@ -74,73 +67,128 @@ public class AdvancementsCache { } /** - * Build a form with all advancement categories - * - * @return The built advancement category menu + * Build and send a form with all advancement categories */ - public SimpleFormWindow buildMenuForm() { - // Cache the language for cleaner access - String language = session.getClientData().getLanguageCode(); + public void buildAndShowMenuForm() { + SimpleForm.Builder builder = + SimpleForm.builder() + .translator(LocaleUtils::getLocaleString, session.getLocale()) + .title("gui.advancements"); - // Created menu window for advancement categories - SimpleFormWindow window = new SimpleFormWindow(LocaleUtils.getLocaleString("gui.advancements", language), ""); + boolean hasAdvancements = false; for (Map.Entry advancement : storedAdvancements.entrySet()) { if (advancement.getValue().getParentId() == null) { // No parent means this is a root advancement - window.getButtons().add(new FormButton(MessageTranslator.convertMessage(advancement.getValue().getDisplayData().getTitle(), language))); + hasAdvancements = true; + builder.button(MessageTranslator.convertMessage(advancement.getValue().getDisplayData().getTitle(), session.getLocale())); } } - if (window.getButtons().isEmpty()) { - window.setContent(LocaleUtils.getLocaleString("advancements.empty", language)); + if (!hasAdvancements) { + builder.content("advancements.empty"); } - return window; + builder.responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + return; + } + + String id = ""; + + int advancementIndex = 0; + for (Map.Entry advancement : storedAdvancements.entrySet()) { + if (advancement.getValue().getParentId() == null) { // Root advancement + if (advancementIndex == response.getClickedButtonId()) { + id = advancement.getKey(); + break; + } else { + advancementIndex++; + } + } + } + + if (!id.equals("")) { + if (id.equals(currentAdvancementCategoryId)) { + // The server thinks we are already on this tab + buildAndShowListForm(); + } else { + // Send a packet indicating that we intend to open this particular advancement window + ClientAdvancementTabPacket packet = new ClientAdvancementTabPacket(id); + session.sendDownstreamPacket(packet); + // Wait for a response there + } + } + }); + + session.sendForm(builder); } /** - * Builds the list of advancements - * - * @return The built list form + * Build and send the list of advancements */ - public SimpleFormWindow buildListForm() { - // Cache the language for easier access - String language = session.getLocale(); - String id = currentAdvancementCategoryId; + public void buildAndShowListForm() { GeyserAdvancement categoryAdvancement = storedAdvancements.get(currentAdvancementCategoryId); + String language = session.getLocale(); - // Create the window - SimpleFormWindow window = new SimpleFormWindow(MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getTitle(), language), - MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getDescription(), language)); + SimpleForm.Builder builder = + SimpleForm.builder() + .title(MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getTitle(), language)) + .content(MessageTranslator.convertMessage(categoryAdvancement.getDisplayData().getDescription(), language)); - if (id != null) { - for (Map.Entry advancementEntry : storedAdvancements.entrySet()) { - GeyserAdvancement advancement = advancementEntry.getValue(); + if (currentAdvancementCategoryId != null) { + for (GeyserAdvancement advancement : storedAdvancements.values()) { if (advancement != null) { if (advancement.getParentId() != null && currentAdvancementCategoryId.equals(advancement.getRootId(this))) { - boolean earned = isEarned(advancement); - - if (earned || !advancement.getDisplayData().isShowToast()) { - window.getButtons().add(new FormButton("§6" + MessageTranslator.convertMessage(advancementEntry.getValue().getDisplayData().getTitle()) + "\n")); - } else { - window.getButtons().add(new FormButton(MessageTranslator.convertMessage(advancementEntry.getValue().getDisplayData().getTitle()) + "\n")); - } + boolean color = isEarned(advancement) || !advancement.getDisplayData().isShowToast(); + builder.button((color ? "§6" : "") + MessageTranslator.convertMessage(advancement.getDisplayData().getTitle()) + '\n'); } } } } - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("gui.back", language))); + builder.button(LanguageUtils.getPlayerLocaleString("gui.back", language)); - return window; + builder.responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + // Indicate that we have closed the current advancement tab + session.sendDownstreamPacket(new ClientAdvancementTabPacket()); + return; + } + + GeyserAdvancement advancement = null; + int advancementIndex = 0; + // Loop around to find the advancement that the client pressed + for (GeyserAdvancement advancementEntry : storedAdvancements.values()) { + if (advancementEntry.getParentId() != null && + currentAdvancementCategoryId.equals(advancementEntry.getRootId(this))) { + if (advancementIndex == response.getClickedButtonId()) { + advancement = advancementEntry; + break; + } else { + advancementIndex++; + } + } + } + + if (advancement != null) { + buildAndShowInfoForm(advancement); + } else { + buildAndShowMenuForm(); + // Indicate that we have closed the current advancement tab + session.sendDownstreamPacket(new ClientAdvancementTabPacket()); + } + }); + + session.sendForm(builder); } /** * Builds the advancement display info based on the chosen category * * @param advancement The advancement used to create the info display - * @return The information for the chosen advancement */ - public SimpleFormWindow buildInfoForm(GeyserAdvancement advancement) { + public void buildAndShowInfoForm(GeyserAdvancement advancement) { // Cache language for easier access String language = session.getLocale(); @@ -160,16 +208,24 @@ public class AdvancementsCache { Parent Advancement: Minecraft // If relevant */ - String content = description + "\n\n§f" + - earnedString + "\n"; + String content = description + "\n\n§f" + earnedString + "\n"; if (!currentAdvancementCategoryId.equals(advancement.getParentId())) { // Only display the parent if it is not the category content += LanguageUtils.getPlayerLocaleString("geyser.advancements.parentid", language, MessageTranslator.convertMessage(storedAdvancements.get(advancement.getParentId()).getDisplayData().getTitle(), language)); } - SimpleFormWindow window = new SimpleFormWindow(MessageTranslator.convertMessage(advancement.getDisplayData().getTitle()), content); - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("gui.back", language))); - return window; + session.sendForm( + SimpleForm.builder() + .title(MessageTranslator.convertMessage(advancement.getDisplayData().getTitle())) + .content(content) + .button(LanguageUtils.getPlayerLocaleString("gui.back", language)) + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (response.isCorrect()) { + buildAndShowListForm(); + } + }) + ); } /** @@ -209,108 +265,6 @@ public class AdvancementsCache { return earned; } - /** - * Handle the menu form response - * - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public boolean handleMenuForm(String response) { - SimpleFormWindow menuForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(ADVANCEMENTS_MENU_FORM_ID); - menuForm.setResponse(response); - - SimpleFormResponse formResponse = (SimpleFormResponse) menuForm.getResponse(); - - String id = ""; - if (formResponse != null && formResponse.getClickedButton() != null) { - int advancementIndex = 0; - for (Map.Entry advancement : storedAdvancements.entrySet()) { - if (advancement.getValue().getParentId() == null) { // Root advancement - if (advancementIndex == formResponse.getClickedButtonId()) { - id = advancement.getKey(); - break; - } else { - advancementIndex++; - } - } - } - } - if (!id.equals("")) { - if (id.equals(currentAdvancementCategoryId)) { - // The server thinks we are already on this tab - session.sendForm(buildListForm(), ADVANCEMENTS_LIST_FORM_ID); - } else { - // Send a packet indicating that we intend to open this particular advancement window - ClientAdvancementTabPacket packet = new ClientAdvancementTabPacket(id); - session.sendDownstreamPacket(packet); - // Wait for a response there - } - } - - return true; - } - - /** - * Handle the list form response (Advancement category choice) - * - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public boolean handleListForm(String response) { - SimpleFormWindow listForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(ADVANCEMENTS_LIST_FORM_ID); - listForm.setResponse(response); - - SimpleFormResponse formResponse = (SimpleFormResponse) listForm.getResponse(); - - if (!listForm.isClosed() && formResponse != null && formResponse.getClickedButton() != null) { - GeyserAdvancement advancement = null; - int advancementIndex = 0; - // Loop around to find the advancement that the client pressed - for (GeyserAdvancement advancementEntry : storedAdvancements.values()) { - if (advancementEntry.getParentId() != null && - currentAdvancementCategoryId.equals(advancementEntry.getRootId(this))) { - if (advancementIndex == formResponse.getClickedButtonId()) { - advancement = advancementEntry; - break; - } else { - advancementIndex++; - } - } - } - if (advancement != null) { - session.sendForm(buildInfoForm(advancement), ADVANCEMENT_INFO_FORM_ID); - } else { - session.sendForm(buildMenuForm(), ADVANCEMENTS_MENU_FORM_ID); - // Indicate that we have closed the current advancement tab - session.sendDownstreamPacket(new ClientAdvancementTabPacket()); - } - } else { - // Indicate that we have closed the current advancement tab - session.sendDownstreamPacket(new ClientAdvancementTabPacket()); - } - - return true; - } - - /** - * Handle the info form response - * - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public boolean handleInfoForm(String response) { - SimpleFormWindow listForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(ADVANCEMENT_INFO_FORM_ID); - listForm.setResponse(response); - - SimpleFormResponse formResponse = (SimpleFormResponse) listForm.getResponse(); - - if (!listForm.isClosed() && formResponse != null && formResponse.getClickedButton() != null) { - session.sendForm(buildListForm(), ADVANCEMENTS_LIST_FORM_ID); - } - - return true; - } - public String getColorFromAdvancementFrameType(GeyserAdvancement advancement) { String base = "\u00a7"; if (advancement.getDisplayData().getFrameType() == Advancement.DisplayData.FrameType.CHALLENGE) { diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/FormCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/FormCache.java new file mode 100644 index 000000000..1cdcf228b --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/FormCache.java @@ -0,0 +1,92 @@ +/* + * 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.connector.network.session.cache; + +import com.nukkitx.protocol.bedrock.packet.ModalFormRequestPacket; +import com.nukkitx.protocol.bedrock.packet.ModalFormResponsePacket; +import com.nukkitx.protocol.bedrock.packet.NetworkStackLatencyPacket; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.RequiredArgsConstructor; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.cumulus.Form; +import org.geysermc.cumulus.SimpleForm; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +@RequiredArgsConstructor +public class FormCache { + private final AtomicInteger formId = new AtomicInteger(0); + private final Int2ObjectMap

forms = new Int2ObjectOpenHashMap<>(); + private final GeyserSession session; + + public int addForm(Form form) { + int windowId = formId.getAndIncrement(); + forms.put(windowId, form); + return windowId; + } + + public int showForm(Form form) { + int windowId = addForm(form); + + ModalFormRequestPacket formRequestPacket = new ModalFormRequestPacket(); + formRequestPacket.setFormId(windowId); + formRequestPacket.setFormData(form.getJsonData()); + session.sendUpstreamPacket(formRequestPacket); + + // Hack to fix the (url) image loading bug + if (form instanceof SimpleForm) { + NetworkStackLatencyPacket latencyPacket = new NetworkStackLatencyPacket(); + latencyPacket.setFromServer(true); + latencyPacket.setTimestamp(-System.currentTimeMillis()); + session.getConnector().getGeneralThreadPool().schedule( + () -> session.sendUpstreamPacket(latencyPacket), + 500, TimeUnit.MILLISECONDS); + } + + return windowId; + } + + public void handleResponse(ModalFormResponsePacket response) { + Form form = forms.get(response.getFormId()); + if (form == null) { + return; + } + + Consumer responseConsumer = form.getResponseHandler(); + if (responseConsumer != null) { + responseConsumer.accept(response.getFormData()); + } + + removeWindow(response.getFormId()); + } + + public boolean removeWindow(int id) { + return forms.remove(id) != null; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java index a114b8bbc..e69de29bb 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/WindowCache.java @@ -1,81 +0,0 @@ -/* - * 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.network.session.cache; - -import com.nukkitx.protocol.bedrock.packet.ModalFormRequestPacket; - -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; - -import lombok.Getter; - -import org.geysermc.common.window.FormWindow; -import org.geysermc.connector.network.session.GeyserSession; - -public class WindowCache { - - private final GeyserSession session; - - @Getter - private final Int2ObjectMap windows = new Int2ObjectOpenHashMap<>(); - - public WindowCache(GeyserSession session) { - this.session = session; - } - - public void addWindow(FormWindow window) { - windows.put(windows.size() + 1, window); - } - - public void addWindow(FormWindow window, int id) { - windows.put(id, window); - } - - public void showWindow(FormWindow window) { - showWindow(window, windows.size() + 1); - } - - public void showWindow(int id) { - if (!windows.containsKey(id)) - return; - - ModalFormRequestPacket formRequestPacket = new ModalFormRequestPacket(); - formRequestPacket.setFormId(id); - formRequestPacket.setFormData(windows.get(id).getJSONData()); - - session.sendUpstreamPacket(formRequestPacket); - } - - public void showWindow(FormWindow window, int id) { - ModalFormRequestPacket formRequestPacket = new ModalFormRequestPacket(); - formRequestPacket.setFormId(id); - formRequestPacket.setFormData(window.getJSONData()); - - session.sendUpstreamPacket(formRequestPacket); - - addWindow(window, id); - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java index 56387fd58..208e7c75d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockNetworkStackLatencyTranslator.java @@ -27,10 +27,17 @@ package org.geysermc.connector.network.translators.bedrock; import com.github.steveice10.mc.protocol.packet.ingame.client.ClientKeepAlivePacket; import com.nukkitx.protocol.bedrock.packet.NetworkStackLatencyPacket; +import com.nukkitx.protocol.bedrock.packet.UpdateAttributesPacket; +import org.geysermc.connector.entity.attribute.Attribute; +import org.geysermc.connector.entity.attribute.AttributeType; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; -import org.geysermc.floodgate.util.DeviceOS; +import org.geysermc.connector.utils.AttributeUtils; +import org.geysermc.floodgate.util.DeviceOs; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; /** * Used to send the forwarded keep alive packet back to the server @@ -40,18 +47,38 @@ public class BedrockNetworkStackLatencyTranslator extends PacketTranslator 0) { + if (session.getConnector().getConfig().isForwardPlayerPing()) { + ClientKeepAlivePacket keepAlivePacket = new ClientKeepAlivePacket(pingId); + session.sendDownstreamPacket(keepAlivePacket); + } + return; + } + + // Hack to fix the url image loading bug + UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); + attributesPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId()); + + Attribute attribute = session.getPlayerEntity().getAttributes().get(AttributeType.EXPERIENCE_LEVEL); + if (attribute != null) { + attributesPacket.setAttributes(Collections.singletonList(AttributeUtils.getBedrockAttribute(attribute))); + } else { + attributesPacket.setAttributes(Collections.singletonList(AttributeUtils.getBedrockAttribute(AttributeType.EXPERIENCE_LEVEL.getAttribute(0)))); + } + + session.getConnector().getGeneralThreadPool().schedule( + () -> session.sendUpstreamPacket(attributesPacket), + 500, TimeUnit.MILLISECONDS); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java index 3f3226280..501ed4468 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockServerSettingsRequestTranslator.java @@ -31,21 +31,22 @@ import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.utils.SettingsUtils; +import org.geysermc.cumulus.CustomForm; import java.util.concurrent.TimeUnit; @Translator(packet = ServerSettingsRequestPacket.class) public class BedrockServerSettingsRequestTranslator extends PacketTranslator { - @Override public void translate(ServerSettingsRequestPacket packet, GeyserSession session) { - SettingsUtils.buildForm(session); + CustomForm window = SettingsUtils.buildForm(session); + int windowId = session.getFormCache().addForm(window); // Fixes https://bugs.mojang.com/browse/MCPE-94012 because of the delay session.getConnector().getGeneralThreadPool().schedule(() -> { ServerSettingsResponsePacket serverSettingsResponsePacket = new ServerSettingsResponsePacket(); - serverSettingsResponsePacket.setFormData(session.getSettingsForm().getJSONData()); - serverSettingsResponsePacket.setFormId(SettingsUtils.SETTINGS_FORM_ID); + serverSettingsResponsePacket.setFormData(window.getJsonData()); + serverSettingsResponsePacket.setFormId(windowId); session.sendUpstreamPacket(serverSettingsResponsePacket); }, 1, TimeUnit.SECONDS); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java index 17a3b3792..aa22ae465 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaAdvancementsTabTranslator.java @@ -36,10 +36,10 @@ import org.geysermc.connector.network.translators.Translator; */ @Translator(packet = ServerAdvancementTabPacket.class) public class JavaAdvancementsTabTranslator extends PacketTranslator { - @Override public void translate(ServerAdvancementTabPacket packet, GeyserSession session) { - session.getAdvancementsCache().setCurrentAdvancementCategoryId(packet.getTabId()); - session.sendForm(session.getAdvancementsCache().buildListForm(), AdvancementsCache.ADVANCEMENTS_LIST_FORM_ID); + AdvancementsCache advancementsCache = session.getAdvancementsCache(); + advancementsCache.setCurrentAdvancementCategoryId(packet.getTabId()); + advancementsCache.buildAndShowListForm(); } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java index 422083ca7..dceb13789 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaJoinGameTranslator.java @@ -34,6 +34,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.server.ServerJoinGamePack import com.nukkitx.protocol.bedrock.data.GameRuleData; import com.nukkitx.protocol.bedrock.data.PlayerPermission; import com.nukkitx.protocol.bedrock.packet.*; +import org.geysermc.connector.common.AuthType; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; @@ -100,6 +101,11 @@ public class JavaJoinGameTranslator extends PacketTranslator { + @Override + public void translate(ServerPluginMessagePacket packet, GeyserSession session) { + // The only plugin messages it has to listen for are Floodgate plugin messages + if (session.getConnector().getDefaultAuthType() != AuthType.FLOODGATE) { + return; + } + + String channel = packet.getChannel(); + + if (channel.equals("floodgate:form")) { + byte[] data = packet.getData(); + + // receive: first byte is form type, second and third are the id, remaining is the form data + // respond: first and second byte id, remaining is form response data + + FormType type = FormType.getByOrdinal(data[0]); + if (type == null) { + throw new NullPointerException( + "Got type " + data[0] + " which isn't a valid form type!"); + } + + String dataString = new String(data, 3, data.length - 3, Charsets.UTF_8); + + Form form = Forms.fromJson(dataString, type); + form.setResponseHandler(response -> { + byte[] raw = response.getBytes(StandardCharsets.UTF_8); + byte[] finalData = new byte[raw.length + 2]; + + finalData[0] = data[1]; + finalData[1] = data[2]; + System.arraycopy(raw, 0, finalData, 2, raw.length); + + session.sendDownstreamPacket(new ClientPluginMessagePacket(channel, finalData)); + }); + session.sendForm(form); + } + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaStatisticsTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaStatisticsTranslator.java index 9bf810e21..247808041 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaStatisticsTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaStatisticsTranslator.java @@ -40,7 +40,7 @@ public class JavaStatisticsTranslator extends PacketTranslator 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.GLOBAL_API_WS_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) { + // 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 SUBSCRIBER_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 + // otherwise it's handled by the Floodgate plugin subscribers + if (subscribersCount != 1) { + break; + } + + String xuid = node.get("xuid").asText(); + GeyserSession session = connector.getPlayerByXuid(xuid); + + if (session != null) { + if (!node.get("success").asBoolean()) { + logger.info("Failed to upload skin for " + session.getName()); + return; + } + + JsonNode data = node.get("data"); + + String value = data.get("value").asText(); + String signature = data.get("signature").asText(); + + byte[] bytes = (value + '\0' + signature) + .getBytes(StandardCharsets.UTF_8); + PluginMessageUtils.sendMessage(session, getSkinChannel(), bytes); + } + break; + case LOG_MESSAGE: + String logMessage = node.get("message").asText(); + switch (node.get("priority").asInt()) { + case -1: + logger.debug("Got a message from skin uploader: " + logMessage); + break; + case 0: + logger.info("Got a message from skin uploader: " +logMessage); + break; + case 1: + logger.error("Got a message from skin uploader: " + logMessage); + break; + default: + logger.info(logMessage); + break; + } + break; + case NEWS_ADDED: + //todo + } + } 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); + } + } catch (JsonProcessingException ignored) { + // ignore invalid json + } catch (Exception e) { + logger.error("Error while handling onClose", e); + } + } + // try to reconnect (which will make a new id and verify token) after a few seconds + reconnectLater(connector); + } + + @Override + public void onError(Exception ex) { + if (ex instanceof ConnectException || ex instanceof SSLException) { + if (logger.isDebug()) { + logger.error("[debug] Got an error", ex); + } + return; + } + 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); + // we don't have to check the result. onClose will handle that for us + connector.getGeneralThreadPool() + .schedule(client::reconnect, 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/skin/SkinProvider.java b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java index 745a23ebd..6574c9350 100644 --- a/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java +++ b/connector/src/main/java/org/geysermc/connector/skin/SkinProvider.java @@ -86,7 +86,7 @@ public class SkinProvider { public static final String EARS_GEOMETRY_SLIM; public static final SkinGeometry SKULL_GEOMETRY; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static { /* Load in the normal ears geometry */ @@ -521,7 +521,7 @@ public class SkinProvider { return null; } - private static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) { + public static BufferedImage scale(BufferedImage bufferedImage, int newWidth, int newHeight) { BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = resized.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); @@ -581,7 +581,6 @@ public class SkinProvider { outputStream.write((rgba >> 24) & 0xFF); } } - return outputStream.toByteArray(); } diff --git a/common/src/main/java/org/geysermc/common/window/button/FormButton.java b/connector/src/main/java/org/geysermc/connector/utils/Constants.java similarity index 60% rename from common/src/main/java/org/geysermc/common/window/button/FormButton.java rename to connector/src/main/java/org/geysermc/connector/utils/Constants.java index d2075d3ff..a0cc9bf80 100644 --- a/common/src/main/java/org/geysermc/common/window/button/FormButton.java +++ b/connector/src/main/java/org/geysermc/connector/utils/Constants.java @@ -23,35 +23,32 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.common.window.button; +package org.geysermc.connector.utils; -import lombok.Getter; -import lombok.Setter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; -public class FormButton { +public final class Constants { + public static final URI GLOBAL_API_WS_URI; + public static final String NTP_SERVER = "time.cloudflare.com"; - @Getter - @Setter - private String text; + public static final Set NEWS_PROJECT_LIST = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("geyser", "floodgate")) + ); - @Getter - private FormImage image; + public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v1/news"; - public FormButton(String text) { - this.text = text; - } - - public FormButton(String text, FormImage image) { - this.text = text; - - if (image.getData() != null && !image.getData().isEmpty()) { - this.image = image; - } - } - - public void setImage(FormImage image) { - if (image.getData() != null && !image.getData().isEmpty()) { - this.image = image; + static { + URI wsUri = null; + try { + wsUri = new URI("wss://api.geysermc.org/ws"); + } catch (URISyntaxException e) { + e.printStackTrace(); } + GLOBAL_API_WS_URI = wsUri; } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java index 5284bbcff..5f5c1f17f 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LanguageUtils.java @@ -60,7 +60,9 @@ public class LanguageUtils { public static void loadGeyserLocale(String locale) { locale = formatLocale(locale); // Don't load the locale if it's already loaded. - if (LOCALE_MAPPINGS.containsKey(locale)) return; + if (LOCALE_MAPPINGS.containsKey(locale)) { + return; + } InputStream localeStream = GeyserConnector.class.getClassLoader().getResourceAsStream("languages/texts/" + locale + ".properties"); @@ -113,7 +115,7 @@ public class LanguageUtils { // Try and get the key from the default locale if (formatString == null) { - properties = LOCALE_MAPPINGS.get(formatLocale(getDefaultLocale())); + properties = LOCALE_MAPPINGS.get(getDefaultLocale()); formatString = properties.getProperty(key); } @@ -125,7 +127,7 @@ public class LanguageUtils { // Final fallback if (formatString == null) { - formatString = key; + return key; } return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values); @@ -151,7 +153,10 @@ public class LanguageUtils { * @return the current default locale */ public static String getDefaultLocale() { - if (CACHED_LOCALE != null) return CACHED_LOCALE; // We definitely know the locale the user is using + if (CACHED_LOCALE != null) { + return CACHED_LOCALE; // We definitely know the locale the user is using + } + String locale; boolean isValid = true; if (GeyserConnector.getInstance() != null && 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 1614f6191..d3d5fa67d 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/LoginEncryptionUtils.java @@ -35,18 +35,17 @@ import com.nukkitx.network.util.Preconditions; import com.nukkitx.protocol.bedrock.packet.LoginPacket; import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket; import com.nukkitx.protocol.bedrock.util.EncryptionUtils; -import org.geysermc.common.window.*; -import org.geysermc.common.window.button.FormButton; -import org.geysermc.common.window.component.InputComponent; -import org.geysermc.common.window.component.LabelComponent; -import org.geysermc.common.window.response.CustomFormResponse; -import org.geysermc.common.window.response.ModalFormResponse; -import org.geysermc.common.window.response.SimpleFormResponse; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.auth.AuthData; import org.geysermc.connector.network.session.auth.BedrockClientData; -import org.geysermc.connector.network.session.cache.WindowCache; +import org.geysermc.cumulus.CustomForm; +import org.geysermc.cumulus.ModalForm; +import org.geysermc.cumulus.SimpleForm; +import org.geysermc.cumulus.response.CustomFormResponse; +import org.geysermc.cumulus.response.ModalFormResponse; +import org.geysermc.cumulus.response.SimpleFormResponse; import javax.crypto.SecretKey; import java.io.IOException; @@ -73,7 +72,7 @@ public class LoginEncryptionUtils { } if (lastKey != null) { - if (!EncryptionUtils.verifyJwt(jwt, lastKey)) return false; + if (!EncryptionUtils.verifyJwt(jwt, lastKey)) return false; } JsonNode payloadNode = JSON_MAPPER.readTree(jwt.getPayload().toString()); @@ -121,7 +120,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) { @@ -132,7 +132,9 @@ public class LoginEncryptionUtils { JWSObject clientJwt = JWSObject.parse(clientData); EncryptionUtils.verifyJwt(clientJwt, identityPublicKey); - session.setClientData(JSON_MAPPER.convertValue(JSON_MAPPER.readTree(clientJwt.getPayload().toBytes()), BedrockClientData.class)); + JsonNode clientDataJson = JSON_MAPPER.readTree(clientJwt.getPayload().toBytes()); + BedrockClientData data = JSON_MAPPER.convertValue(clientDataJson, BedrockClientData.class); + session.setClientData(data); if (EncryptionUtils.canUseEncryption()) { try { @@ -176,132 +178,118 @@ public class LoginEncryptionUtils { } } - private static final int AUTH_MSA_DETAILS_FORM_ID = 1334; - private static final int AUTH_MSA_CODE_FORM_ID = 1335; - private static final int AUTH_FORM_ID = 1336; - private static final int AUTH_DETAILS_FORM_ID = 1337; - - public static void showLoginWindow(GeyserSession session) { + public static void buildAndShowLoginWindow(GeyserSession session) { // Set DoDaylightCycle to false so the time doesn't accelerate while we're here session.setDaylightCycle(false); - String userLanguage = session.getLocale(); - SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.title", userLanguage), LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.desc", userLanguage)); - if (session.getConnector().getConfig().getRemote().isPasswordAuthentication()) { - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.mojang", userLanguage))); - } - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage))); - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); + GeyserConfiguration config = session.getConnector().getConfig(); + boolean isPasswordAuthEnabled = config.getRemote().isPasswordAuthentication(); - session.sendForm(window, AUTH_FORM_ID); + session.sendForm( + SimpleForm.builder() + .translator(LanguageUtils::getPlayerLocaleString, session.getLocale()) + .title("geyser.auth.login.form.notice.title") + .content("geyser.auth.login.form.notice.desc") + .optionalButton("geyser.auth.login.form.notice.btn_login.mojang", isPasswordAuthEnabled) + .button("geyser.auth.login.form.notice.btn_login.microsoft") + .button("geyser.auth.login.form.notice.btn_disconnect") + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + buildAndShowLoginWindow(session); + return; + } + + if (isPasswordAuthEnabled && response.getClickedButtonId() == 0) { + session.setMicrosoftAccount(false); + buildAndShowLoginDetailsWindow(session); + return; + } + + if (isPasswordAuthEnabled && response.getClickedButtonId() == 1) { + session.setMicrosoftAccount(true); + buildAndShowMicrosoftAuthenticationWindow(session); + return; + } + + if (response.getClickedButtonId() == 0) { + // Just show the OAuth code + session.authenticateWithMicrosoftCode(); + return; + } + + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + })); } - public static void showLoginDetailsWindow(GeyserSession session) { - String userLanguage = session.getLocale(); - CustomFormWindow window = new CustomFormBuilder(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.details.title", userLanguage)) - .addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.details.desc", userLanguage))) - .addComponent(new InputComponent(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.details.email", userLanguage), "account@geysermc.org", "")) - .addComponent(new InputComponent(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.details.pass", userLanguage), "123456", "")) - .build(); + public static void buildAndShowLoginDetailsWindow(GeyserSession session) { + session.sendForm( + CustomForm.builder() + .translator(LanguageUtils::getPlayerLocaleString, session.getLocale()) + .title("geyser.auth.login.form.details.title") + .label("geyser.auth.login.form.details.desc") + .input("geyser.auth.login.form.details.email", "account@geysermc.org", "") + .input("geyser.auth.login.form.details.pass", "123456", "") + .responseHandler((form, responseData) -> { + CustomFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + buildAndShowLoginDetailsWindow(session); + return; + } - session.sendForm(window, AUTH_DETAILS_FORM_ID); + session.authenticate(response.next(), response.next()); + })); } /** * Prompts the user between either OAuth code login or manual password authentication */ - public static void showMicrosoftAuthenticationWindow(GeyserSession session) { - String userLanguage = session.getLocale(); - SimpleFormWindow window = new SimpleFormWindow(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_login.microsoft", userLanguage), ""); - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.browser", userLanguage))); - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.method.password", userLanguage))); // This form won't show if password authentication is disabled - window.getButtons().add(new FormButton(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.notice.btn_disconnect", userLanguage))); - session.sendForm(window, AUTH_MSA_DETAILS_FORM_ID); + public static void buildAndShowMicrosoftAuthenticationWindow(GeyserSession session) { + session.sendForm( + SimpleForm.builder() + .translator(LanguageUtils::getPlayerLocaleString, session.getLocale()) + .title("geyser.auth.login.form.notice.btn_login.microsoft") + .button("geyser.auth.login.method.browser") + .button("geyser.auth.login.method.password") + .button("geyser.auth.login.form.notice.btn_disconnect") + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + buildAndShowLoginWindow(session); + return; + } + + if (response.getClickedButtonId() == 0) { + session.authenticateWithMicrosoftCode(); + } else if (response.getClickedButtonId() == 1) { + buildAndShowLoginDetailsWindow(session); + } else { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + })); } /** * Shows the code that a user must input into their browser */ - public static void showMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse response) { - ModalFormWindow msaCodeWindow = new ModalFormWindow("%xbox.signin", "%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + - response.user_code, "%gui.done", "%menu.disconnect"); - session.sendForm(msaCodeWindow, LoginEncryptionUtils.AUTH_MSA_CODE_FORM_ID); - } - - public static boolean authenticateFromForm(GeyserSession session, GeyserConnector connector, int formId, String formData) { - WindowCache windowCache = session.getWindowCache(); - if (!windowCache.getWindows().containsKey(formId)) - return false; - - if (formId == AUTH_MSA_DETAILS_FORM_ID || formId == AUTH_FORM_ID || formId == AUTH_DETAILS_FORM_ID || formId == AUTH_MSA_CODE_FORM_ID) { - FormWindow window = windowCache.getWindows().remove(formId); - window.setResponse(formData.trim()); - - if (!session.isLoggedIn()) { - if (formId == AUTH_DETAILS_FORM_ID && window instanceof CustomFormWindow) { - CustomFormWindow customFormWindow = (CustomFormWindow) window; - - CustomFormResponse response = (CustomFormResponse) customFormWindow.getResponse(); - if (response != null) { - String email = response.getInputResponses().get(1); - String password = response.getInputResponses().get(2); - - session.authenticate(email, password); - - // Clear windows so authentication data isn't accidentally cached - windowCache.getWindows().clear(); - } else { - showLoginDetailsWindow(session); - } - } else if (formId == AUTH_FORM_ID && window instanceof SimpleFormWindow) { - boolean isPasswordAuthentication = session.getConnector().getConfig().getRemote().isPasswordAuthentication(); - int microsoftButton = isPasswordAuthentication ? 1 : 0; - int disconnectButton = isPasswordAuthentication ? 2 : 1; - SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); - if (response != null) { - if (isPasswordAuthentication && response.getClickedButtonId() == 0) { - session.setMicrosoftAccount(false); - showLoginDetailsWindow(session); - } else if (response.getClickedButtonId() == microsoftButton) { - session.setMicrosoftAccount(true); - if (isPasswordAuthentication) { - showMicrosoftAuthenticationWindow(session); - } else { - // Just show the OAuth code - session.authenticateWithMicrosoftCode(); + public static void buildAndShowMicrosoftCodeWindow(GeyserSession session, MsaAuthenticationService.MsCodeResponse msCode) { + session.sendForm( + ModalForm.builder() + .title("%xbox.signin") + .content("%xbox.signin.website\n%xbox.signin.url\n%xbox.signin.enterCode\n" + msCode.user_code) + .button1("%gui.done") + .button2("%menu.disconnect") + .responseHandler((form, responseData) -> { + ModalFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + buildAndShowMicrosoftAuthenticationWindow(session); + return; } - } else if (response.getClickedButtonId() == disconnectButton) { - session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); - } - } else { - showLoginWindow(session); - } - } else if (formId == AUTH_MSA_DETAILS_FORM_ID && window instanceof SimpleFormWindow) { - SimpleFormResponse response = (SimpleFormResponse) window.getResponse(); - if (response != null) { - if (response.getClickedButtonId() == 0) { - session.authenticateWithMicrosoftCode(); - } else if (response.getClickedButtonId() == 1) { - showLoginDetailsWindow(session); - } else if (response.getClickedButtonId() == 2) { - session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); - } - } else { - showLoginWindow(session); - } - } else if (formId == AUTH_MSA_CODE_FORM_ID && window instanceof ModalFormWindow) { - ModalFormResponse response = (ModalFormResponse) window.getResponse(); - if (response != null) { - if (response.getClickedButtonId() == 1) { - session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); - } - } else { - showMicrosoftAuthenticationWindow(session); - } - } - } - } - return true; - } + if (response.getClickedButtonId() == 1) { + session.disconnect(LanguageUtils.getPlayerLocaleString("geyser.auth.login.form.disconnect", session.getLocale())); + } + }) + ); + } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/NewsHandler.java b/connector/src/main/java/org/geysermc/connector/utils/NewsHandler.java new file mode 100644 index 000000000..aa8fe2efc --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/utils/NewsHandler.java @@ -0,0 +1,175 @@ +/* + * 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 com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.GeyserLogger; +import org.geysermc.connector.common.ChatColor; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.floodgate.news.NewsItem; +import org.geysermc.floodgate.news.NewsItemAction; +import org.geysermc.floodgate.news.data.BuildSpecificData; +import org.geysermc.floodgate.news.data.CheckAfterData; + +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class NewsHandler { + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + private final GeyserLogger logger = GeyserConnector.getInstance().getLogger(); + private final Gson gson = new Gson(); + + private final Map activeNewsItems = new HashMap<>(); + private final String branch; + private final int build; + + private boolean geyserStarted; + + public NewsHandler(String branch, int build) { + this.branch = branch; + this.build = build; + + executorService.scheduleWithFixedDelay(this::checkNews, 0, 30, TimeUnit.MINUTES); + } + + private void schedule(long delayMs) { + executorService.schedule(this::checkNews, delayMs, TimeUnit.MILLISECONDS); + } + + private void checkNews() { + try { + String body = WebUtils.getBody(Constants.NEWS_OVERVIEW_URL); + JsonArray array = gson.fromJson(body, JsonArray.class); + + try { + for (JsonElement newsItemElement : array) { + NewsItem newsItem = NewsItem.readItem(newsItemElement.getAsJsonObject()); + if (newsItem != null) { + addNews(newsItem); + } + } + } catch (Exception e) { + if (logger.isDebug()) { + logger.error("Error while reading news item", e); + } + } + } catch (JsonSyntaxException ignored) {} + } + + public void setGeyserStarted() { + geyserStarted = true; + } + + public void handleNews(GeyserSession session, NewsItemAction action) { + for (NewsItem news : getActiveNews(action)) { + handleNewsItem(session, news, action); + } + } + + private void handleNewsItem(GeyserSession session, NewsItem news, NewsItemAction action) { + switch (action) { + case ON_SERVER_STARTED: + if (!geyserStarted) { + return; + } + case BROADCAST_TO_CONSOLE: + logger.info(news.getMessage()); + break; + case ON_OPERATOR_JOIN: + //todo doesn't work, it's called before we know the op level. +// if (session != null && session.getOpPermissionLevel() >= 2) { +// session.sendMessage(ChatColor.GREEN + news.getMessage()); +// } + break; + case BROADCAST_TO_OPERATORS: + for (GeyserSession player : GeyserConnector.getInstance().getPlayers()) { + if (player.getOpPermissionLevel() >= 2) { + session.sendMessage(ChatColor.GREEN + news.getMessage()); + } + } + break; + } + } + + public Collection getActiveNews() { + return activeNewsItems.values(); + } + + public Collection getActiveNews(NewsItemAction action) { + List news = new ArrayList<>(); + for (NewsItem item : getActiveNews()) { + if (item.getActions().contains(action)) { + news.add(item); + } + } + return news; + } + + public void addNews(NewsItem item) { + if (activeNewsItems.containsKey(item.getId())) { + if (!item.isActive()) { + activeNewsItems.remove(item.getId()); + } + return; + } + + if (!Constants.NEWS_PROJECT_LIST.contains(item.getProject())) { + return; + } + + switch (item.getType()) { + case BUILD_SPECIFIC: + if (!item.getDataAs(BuildSpecificData.class).isAffected(branch, build)) { + return; + } + break; + case CHECK_AFTER: + long checkAfter = item.getDataAs(CheckAfterData.class).getCheckAfter(); + long delayMs = System.currentTimeMillis() - checkAfter; + schedule(delayMs > 0 ? delayMs : 0); + break; + } + + activeNewsItems.put(item.getId(), item); + activateNews(item); + } + + private void activateNews(NewsItem item) { + for (NewsItemAction action : item.getActions()) { + handleNewsItem(null, item, action); + } + } + + public void shutdown() { + executorService.shutdown(); + } +} 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 c5a7d9e8d..a914f699e 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/PluginMessageUtils.java @@ -25,35 +25,63 @@ package org.geysermc.connector.utils; +import com.github.steveice10.mc.protocol.packet.ingame.client.ClientPluginMessagePacket; +import com.google.common.base.Charsets; import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; -import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; public class PluginMessageUtils { - - private static final byte[] BRAND_DATA; + private static final String SKIN_CHANNEL = "floodgate:skin"; + private static final byte[] GEYSER_BRAND_DATA; + private static final byte[] FLOODGATE_REGISTER_DATA; static { - byte[] data = GeyserConnector.NAME.getBytes(StandardCharsets.UTF_8); - byte[] varInt = writeVarInt(data.length); - BRAND_DATA = new byte[varInt.length + data.length]; - System.arraycopy(varInt, 0, BRAND_DATA, 0, varInt.length); - System.arraycopy(data, 0, BRAND_DATA, varInt.length, data.length); + byte[] data = GeyserConnector.NAME.getBytes(Charsets.UTF_8); + GEYSER_BRAND_DATA = + ByteBuffer.allocate(data.length + getVarIntLength(data.length)) + .put(writeVarInt(data.length)) + .put(data) + .array(); + + FLOODGATE_REGISTER_DATA = (SKIN_CHANNEL + "\0floodgate:form").getBytes(Charsets.UTF_8); } /** * Get the prebuilt brand as a byte array + * * @return the brand information of the Geyser client */ public static byte[] getGeyserBrandData() { - return BRAND_DATA; + return GEYSER_BRAND_DATA; + } + + /** + * Get the prebuilt register data as a byte array + * + * @return the register data of the Floodgate channels + */ + public static byte[] getFloodgateRegisterData() { + 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) { + session.sendDownstreamPacket(new ClientPluginMessagePacket(channel, data)); } private static byte[] writeVarInt(int value) { byte[] data = new byte[getVarIntLength(value)]; int index = 0; do { - byte temp = (byte)(value & 0b01111111); + byte temp = (byte) (value & 0b01111111); value >>>= 7; if (value != 0) { temp |= 0b10000000; diff --git a/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java index d0cfce862..0e2a54a3c 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/SettingsUtils.java @@ -27,79 +27,69 @@ package org.geysermc.connector.utils; import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; -import org.geysermc.common.window.CustomFormBuilder; -import org.geysermc.common.window.CustomFormWindow; -import org.geysermc.common.window.button.FormImage; -import org.geysermc.common.window.component.DropdownComponent; -import org.geysermc.common.window.component.InputComponent; -import org.geysermc.common.window.component.LabelComponent; -import org.geysermc.common.window.component.ToggleComponent; -import org.geysermc.common.window.response.CustomFormResponse; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.world.WorldManager; +import org.geysermc.cumulus.CustomForm; +import org.geysermc.cumulus.component.DropdownComponent; +import org.geysermc.cumulus.response.CustomFormResponse; import java.util.ArrayList; public class SettingsUtils { - - // Used in UpstreamPacketHandler.java - public static final int SETTINGS_FORM_ID = 1338; - /** * Build a settings form for the given session and store it for later * * @param session The session to build the form for */ - public static void buildForm(GeyserSession session) { + public static CustomForm buildForm(GeyserSession session) { // Cache the language for cleaner access String language = session.getLocale(); - CustomFormBuilder builder = new CustomFormBuilder(LanguageUtils.getPlayerLocaleString("geyser.settings.title.main", language)); - builder.setIcon(new FormImage(FormImage.FormImageType.PATH, "textures/ui/settings_glyph_color_2x.png")); + CustomForm.Builder builder = CustomForm.builder() + .translator(LanguageUtils::getPlayerLocaleString, language) + .title("geyser.settings.title.main") + .iconPath("textures/ui/settings_glyph_color_2x.png"); // Only show the client title if any of the client settings are available if (session.getPreferencesCache().isAllowShowCoordinates() || CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { - builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.client", language))); + builder.label("geyser.settings.title.client"); // Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config. if (session.getPreferencesCache().isAllowShowCoordinates()) { - builder.addComponent(new ToggleComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.option.coordinates", language), session.getPreferencesCache().isPrefersShowCoordinates())); + builder.toggle("geyser.settings.option.coordinates", session.getPreferencesCache().isPrefersShowCoordinates()); } if (CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { - DropdownComponent cooldownDropdown = new DropdownComponent(); - cooldownDropdown.setText(LocaleUtils.getLocaleString("options.attackIndicator", language)); - cooldownDropdown.setOptions(new ArrayList<>()); - cooldownDropdown.addOption(LocaleUtils.getLocaleString("options.attack.crosshair", language), session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.TITLE); - cooldownDropdown.addOption(LocaleUtils.getLocaleString("options.attack.hotbar", language), session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.ACTIONBAR); - cooldownDropdown.addOption(LocaleUtils.getLocaleString("options.off", language), session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.DISABLED); - builder.addComponent(cooldownDropdown); + DropdownComponent.Builder cooldownDropdown = DropdownComponent.builder("options.attackIndicator"); + cooldownDropdown.option("options.attack.crosshair", session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.TITLE); + cooldownDropdown.option("options.attack.hotbar", session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.ACTIONBAR); + cooldownDropdown.option("options.off", session.getPreferencesCache().getCooldownPreference() == CooldownUtils.CooldownType.DISABLED); + builder.dropdown(cooldownDropdown); } } - if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) { - builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.server", language))); + builder.label("geyser.settings.title.server"); - DropdownComponent gamemodeDropdown = new DropdownComponent(); - gamemodeDropdown.setText("%createWorldScreen.gameMode.personal"); - gamemodeDropdown.setOptions(new ArrayList<>()); + DropdownComponent.Builder gamemodeDropdown = DropdownComponent.builder("%createWorldScreen.gameMode.personal"); for (GameMode gamemode : GameMode.values()) { - gamemodeDropdown.addOption(LocaleUtils.getLocaleString("selectWorld.gameMode." + gamemode.name().toLowerCase(), language), session.getGameMode() == gamemode); + gamemodeDropdown.option("selectWorld.gameMode." + gamemode.name().toLowerCase(), session.getGameMode() == gamemode); } - builder.addComponent(gamemodeDropdown); + builder.dropdown(gamemodeDropdown); - DropdownComponent difficultyDropdown = new DropdownComponent(); - difficultyDropdown.setText("%options.difficulty"); - difficultyDropdown.setOptions(new ArrayList<>()); + DropdownComponent.Builder difficultyDropdown = DropdownComponent.builder("%options.difficulty"); for (Difficulty difficulty : Difficulty.values()) { - difficultyDropdown.addOption("%options.difficulty." + difficulty.name().toLowerCase(), session.getWorldCache().getDifficulty() == difficulty); + difficultyDropdown.option("%options.difficulty." + difficulty.name().toLowerCase(), session.getWorldCache().getDifficulty() == difficulty); } - builder.addComponent(difficultyDropdown); + builder.dropdown(difficultyDropdown); } if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules")) { - builder.addComponent(new LabelComponent(LanguageUtils.getPlayerLocaleString("geyser.settings.title.game_rules", language))); + builder.label("geyser.settings.title.game_rules") + .translator(LocaleUtils::getLocaleString); // we need translate gamerules next + + WorldManager worldManager = GeyserConnector.getInstance().getWorldManager(); for (GameRule gamerule : GameRule.values()) { if (gamerule.equals(GameRule.UNKNOWN)) { continue; @@ -107,89 +97,69 @@ public class SettingsUtils { // Add the relevant form item based on the gamerule type if (Boolean.class.equals(gamerule.getType())) { - builder.addComponent(new ToggleComponent(LocaleUtils.getLocaleString("gamerule." + gamerule.getJavaID(), language), GeyserConnector.getInstance().getWorldManager().getGameRuleBool(session, gamerule))); + builder.toggle("gamerule." + gamerule.getJavaID(), worldManager.getGameRuleBool(session, gamerule)); } else if (Integer.class.equals(gamerule.getType())) { - builder.addComponent(new InputComponent(LocaleUtils.getLocaleString("gamerule." + gamerule.getJavaID(), language), "", String.valueOf(GeyserConnector.getInstance().getWorldManager().getGameRuleInt(session, gamerule)))); + builder.input("gamerule." + gamerule.getJavaID(), "", String.valueOf(worldManager.getGameRuleInt(session, gamerule))); } } } - session.setSettingsForm(builder.build()); - } - - /** - * Handle the settings form response - * - * @param session The session that sent the response - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public static boolean handleSettingsForm(GeyserSession session, String response) { - CustomFormWindow settingsForm = session.getSettingsForm(); - settingsForm.setResponse(response); - - CustomFormResponse settingsResponse = (CustomFormResponse) settingsForm.getResponse(); - if (settingsResponse == null) { - return false; - } - int offset = 0; - - if (session.getPreferencesCache().isAllowShowCoordinates() || CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { - offset++; // Client settings title - - // Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config. - if (session.getPreferencesCache().isAllowShowCoordinates()) { - session.getPreferencesCache().setPrefersShowCoordinates(settingsResponse.getToggleResponses().get(offset)); - session.getPreferencesCache().updateShowCoordinates(); - offset++; + builder.responseHandler((form, responseData) -> { + CustomFormResponse response = form.parseResponse(responseData); + if (response.isClosed() || response.isInvalid()) { + return; } - if (CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { - CooldownUtils.CooldownType cooldownType = CooldownUtils.CooldownType.VALUES[settingsResponse.getDropdownResponses().get(offset).getElementID()]; - session.getPreferencesCache().setCooldownPreference(cooldownType); - offset++; - } - } + if (session.getPreferencesCache().isAllowShowCoordinates() || CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { + response.skip(); // Client settings title - if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) { - offset++; // Server settings title - - GameMode gameMode = GameMode.values()[settingsResponse.getDropdownResponses().get(offset).getElementID()]; - if (gameMode != null && gameMode != session.getGameMode()) { - session.getConnector().getWorldManager().setPlayerGameMode(session, gameMode); - } - offset++; - - Difficulty difficulty = Difficulty.values()[settingsResponse.getDropdownResponses().get(offset).getElementID()]; - if (difficulty != null && difficulty != session.getWorldCache().getDifficulty()) { - session.getConnector().getWorldManager().setDifficulty(session, difficulty); - } - offset++; - } - - if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules")) { - offset++; // Game rule title - - for (GameRule gamerule : GameRule.values()) { - if (gamerule.equals(GameRule.UNKNOWN)) { - continue; + // Client can only see its coordinates if reducedDebugInfo is disabled and coordinates are enabled in geyser config. + if (session.getPreferencesCache().isAllowShowCoordinates()) { + session.getPreferencesCache().setPrefersShowCoordinates(response.next()); + session.getPreferencesCache().updateShowCoordinates(); + response.skip(); } - if (Boolean.class.equals(gamerule.getType())) { - boolean value = settingsResponse.getToggleResponses().get(offset); - if (value != session.getConnector().getWorldManager().getGameRuleBool(session, gamerule)) { - session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); + if (CooldownUtils.getDefaultShowCooldown() != CooldownUtils.CooldownType.DISABLED) { + CooldownUtils.CooldownType cooldownType = CooldownUtils.CooldownType.VALUES[(int) response.next()]; + session.getPreferencesCache().setCooldownPreference(cooldownType); + response.skip(); + } + } + + if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) { + GameMode gameMode = GameMode.values()[(int) response.next()]; + if (gameMode != null && gameMode != session.getGameMode()) { + session.getConnector().getWorldManager().setPlayerGameMode(session, gameMode); + } + + Difficulty difficulty = Difficulty.values()[(int) response.next()]; + if (difficulty != null && difficulty != session.getWorldCache().getDifficulty()) { + session.getConnector().getWorldManager().setDifficulty(session, difficulty); + } + } + + if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.gamerules")) { + for (GameRule gamerule : GameRule.values()) { + if (gamerule.equals(GameRule.UNKNOWN)) { + continue; } - } else if (Integer.class.equals(gamerule.getType())) { - int value = Integer.parseInt(settingsResponse.getInputResponses().get(offset)); - if (value != session.getConnector().getWorldManager().getGameRuleInt(session, gamerule)) { - session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); + + if (Boolean.class.equals(gamerule.getType())) { + boolean value = response.next(); + if (value != session.getConnector().getWorldManager().getGameRuleBool(session, gamerule)) { + session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); + } + } else if (Integer.class.equals(gamerule.getType())) { + int value = Integer.parseInt(response.next()); + if (value != session.getConnector().getWorldManager().getGameRuleInt(session, gamerule)) { + session.getConnector().getWorldManager().setGameRule(session, gamerule.getJavaID(), value); + } } } - offset++; } - } + }); - return true; + return builder.build(); } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java b/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java index 08521284b..e59807d75 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/StatisticsUtils.java @@ -28,197 +28,171 @@ package org.geysermc.connector.utils; import com.github.steveice10.mc.protocol.data.MagicValues; import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType; import com.github.steveice10.mc.protocol.data.game.statistic.*; -import org.geysermc.common.window.SimpleFormWindow; -import org.geysermc.common.window.button.FormButton; -import org.geysermc.common.window.button.FormImage; -import org.geysermc.common.window.response.SimpleFormResponse; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; +import org.geysermc.cumulus.SimpleForm; +import org.geysermc.cumulus.response.SimpleFormResponse; +import org.geysermc.cumulus.util.FormImage; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class StatisticsUtils { - - // Used in UpstreamPacketHandler.java - public static final int STATISTICS_MENU_FORM_ID = 1339; - public static final int STATISTICS_LIST_FORM_ID = 1340; + private static final Pattern CONTENT_PATTERN = Pattern.compile("^\\S+:", Pattern.MULTILINE); /** * Build a form for the given session with all statistic categories * * @param session The session to build the form for */ - public static SimpleFormWindow buildMenuForm(GeyserSession session) { + public static void buildAndSendStatisticsMenu(GeyserSession session) { // Cache the language for cleaner access - String language = session.getClientData().getLanguageCode(); + String language = session.getLocale(); - SimpleFormWindow window = new SimpleFormWindow(LocaleUtils.getLocaleString("gui.stats", language), ""); + session.sendForm( + SimpleForm.builder() + .translator(StatisticsUtils::translate, language) + .title("gui.stats") + .button("stat.generalButton", FormImage.Type.PATH, "textures/ui/World") + .button("stat.itemsButton - stat_type.minecraft.mined", FormImage.Type.PATH, "textures/items/iron_pickaxe") + .button("stat.itemsButton - stat_type.minecraft.broken", FormImage.Type.PATH, "textures/item/record_11") + .button("stat.itemsButton - stat_type.minecraft.crafted", FormImage.Type.PATH, "textures/blocks/crafting_table_side") + .button("stat.itemsButton - stat_type.minecraft.used", FormImage.Type.PATH, "textures/ui/Wrenches1") + .button("stat.itemsButton - stat_type.minecraft.picked_up", FormImage.Type.PATH, "textures/blocks/chest_front") + .button("stat.itemsButton - stat_type.minecraft.dropped", FormImage.Type.PATH, "textures/ui/trash_default") + .button("stat.mobsButton - geyser.statistics.killed", FormImage.Type.PATH, "textures/items/diamon_sword") + .button("stat.mobsButton - geyser.statistics.killed_by", FormImage.Type.PATH, "textures/ui/wither_heart_flash") + .responseHandler((form, responseData) -> { + SimpleFormResponse response = form.parseResponse(responseData); + if (!response.isCorrect()) { + return; + } - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.generalButton", language), new FormImage(FormImage.FormImageType.PATH, "textures/ui/World"))); + SimpleForm.Builder builder = + SimpleForm.builder() + .translator(StatisticsUtils::translate, language); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.mined", language), new FormImage(FormImage.FormImageType.PATH, "textures/items/iron_pickaxe"))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.broken", language), new FormImage(FormImage.FormImageType.PATH, "textures/items/record_11"))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.crafted", language), new FormImage(FormImage.FormImageType.PATH, "textures/blocks/crafting_table_side"))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.used", language), new FormImage(FormImage.FormImageType.PATH, "textures/ui/Wrenches1"))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.picked_up", language), new FormImage(FormImage.FormImageType.PATH, "textures/blocks/chest_front"))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.dropped", language), new FormImage(FormImage.FormImageType.PATH, "textures/ui/trash_default"))); + StringBuilder content = new StringBuilder(); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed", language), new FormImage(FormImage.FormImageType.PATH, "textures/items/diamond_sword"))); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed_by", language), new FormImage(FormImage.FormImageType.PATH, "textures/ui/wither_heart_flash"))); + switch (response.getClickedButtonId()) { + case 0: + builder.title("stat.generalButton"); - return window; - } + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof GenericStatistic) { + String statName = ((GenericStatistic) entry.getKey()).name().toLowerCase(); + content.append("stat.minecraft.").append(statName).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 1: + builder.title("stat.itemsButton - stat_type.minecraft.mined"); - /** - * Handle the menu form response - * - * @param session The session that sent the response - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public static boolean handleMenuForm(GeyserSession session, String response) { - SimpleFormWindow menuForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(STATISTICS_MENU_FORM_ID); - menuForm.setResponse(response); - SimpleFormResponse formResponse = (SimpleFormResponse) menuForm.getResponse(); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof BreakBlockStatistic) { + String block = BlockTranslator.JAVA_ID_TO_JAVA_IDENTIFIER_MAP.get(((BreakBlockStatistic) entry.getKey()).getId()); + block = block.replace("minecraft:", "block.minecraft."); + content.append(block).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 2: + builder.title("stat.itemsButton - stat_type.minecraft.broken"); - // Cache the language for cleaner access - String language = session.getClientData().getLanguageCode(); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof BreakItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((BreakItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 3: + builder.title("stat.itemsButton - stat_type.minecraft.crafted"); - if (formResponse != null && formResponse.getClickedButton() != null) { - String title; - StringBuilder content = new StringBuilder(); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof CraftItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((CraftItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 4: + builder.title("stat.itemsButton - stat_type.minecraft.used"); - switch (formResponse.getClickedButtonId()) { - case 0: - title = LocaleUtils.getLocaleString("stat.generalButton", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof UseItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((UseItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 5: + builder.title("stat.itemsButton - stat_type.minecraft.picked_up"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof GenericStatistic) { - content.append(LocaleUtils.getLocaleString("stat.minecraft." + ((GenericStatistic) entry.getKey()).name().toLowerCase(), language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 1: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.mined", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof PickupItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((PickupItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 6: + builder.title("stat.itemsButton - stat_type.minecraft.dropped"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof BreakBlockStatistic) { - String block = BlockTranslator.JAVA_ID_TO_JAVA_IDENTIFIER_MAP.get(((BreakBlockStatistic) entry.getKey()).getId()); - block = block.replace("minecraft:", "block.minecraft."); - block = LocaleUtils.getLocaleString(block, language); - content.append(block + ": " + entry.getValue() + "\n"); - } - } - break; - case 2: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.broken", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof DropItemStatistic) { + String item = ItemRegistry.ITEM_ENTRIES.get(((DropItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); + content.append(getItemTranslateKey(item, language)).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 7: + builder.title("stat.mobsButton - geyser.statistics.killed"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof BreakItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((BreakItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 3: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.crafted", language); + for (Map.Entry entry : session.getStatistics().entrySet()) { + if (entry.getKey() instanceof KillEntityStatistic) { + String entityName = MagicValues.key(EntityType.class, ((KillEntityStatistic) entry.getKey()).getId()).name().toLowerCase(); + content.append("entity.minecraft.").append(entityName).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + case 8: + builder.title("stat.mobsButton - geyser.statistics.killed_by"); - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof CraftItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((CraftItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 4: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.used", language); + for (Map.Entry entry : session + .getStatistics().entrySet()) { + if (entry.getKey() instanceof KilledByEntityStatistic) { + String entityName = MagicValues.key(EntityType.class, ((KilledByEntityStatistic) entry.getKey()).getId()).name().toLowerCase(); + content.append("entity.minecraft.").append(entityName).append(": ").append(entry.getValue()).append("\n"); + } + } + break; + default: + return; + } - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof UseItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((UseItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 5: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.picked_up", language); + if (content.length() == 0) { + content = new StringBuilder("geyser.statistics.none"); + } - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof PickupItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((PickupItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 6: - title = LocaleUtils.getLocaleString("stat.itemsButton", language) + " - " + LocaleUtils.getLocaleString("stat_type.minecraft.dropped", language); - - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof DropItemStatistic) { - String item = ItemRegistry.ITEM_ENTRIES.get(((DropItemStatistic) entry.getKey()).getId()).getJavaIdentifier(); - content.append(getItemTranslateKey(item, language) + ": " + entry.getValue() + "\n"); - } - } - break; - case 7: - title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed", language); - - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof KillEntityStatistic) { - String mob = LocaleUtils.getLocaleString("entity.minecraft." + MagicValues.key(EntityType.class, ((KillEntityStatistic) entry.getKey()).getId()).name().toLowerCase(), language); - content.append(mob + ": " + entry.getValue() + "\n"); - } - } - break; - case 8: - title = LocaleUtils.getLocaleString("stat.mobsButton", language) + " - " + LanguageUtils.getPlayerLocaleString("geyser.statistics.killed_by", language); - - for (Map.Entry entry : session.getStatistics().entrySet()) { - if (entry.getKey() instanceof KilledByEntityStatistic) { - String mob = LocaleUtils.getLocaleString("entity.minecraft." + MagicValues.key(EntityType.class, ((KilledByEntityStatistic) entry.getKey()).getId()).name().toLowerCase(), language); - content.append(mob + ": " + entry.getValue() + "\n"); - } - } - break; - default: - return false; - } - - if (content.length() == 0) { - content = new StringBuilder(LanguageUtils.getPlayerLocaleString("geyser.statistics.none", language)); - } - - SimpleFormWindow window = new SimpleFormWindow(title, content.toString()); - window.getButtons().add(new FormButton(LocaleUtils.getLocaleString("gui.back", language), new FormImage(FormImage.FormImageType.PATH, "textures/gui/newgui/undo"))); - session.sendForm(window, STATISTICS_LIST_FORM_ID); - } - - return true; - } - - /** - * Handle the list form response - * - * @param session The session that sent the response - * @param response The response string to parse - * @return True if the form was parsed correctly, false if not - */ - public static boolean handleListForm(GeyserSession session, String response) { - SimpleFormWindow listForm = (SimpleFormWindow) session.getWindowCache().getWindows().get(STATISTICS_LIST_FORM_ID); - listForm.setResponse(response); - - if (!listForm.isClosed()) { - session.sendForm(buildMenuForm(session), STATISTICS_MENU_FORM_ID); - } - - return true; + session.sendForm( + builder.content(content.toString()) + .button("gui.back", FormImage.Type.PATH, "textures/gui/newgui/undo") + .responseHandler((form1, subFormResponseData) -> { + SimpleFormResponse response1 = form.parseResponse(subFormResponseData); + if (response1.isCorrect()) { + buildAndSendStatisticsMenu(session); + } + })); + })); } /** * Finds the item translation key from the Java locale. - * - * @param item the namespaced item to search for. + * + * @param item the namespaced item to search for. * @param language the language to search in * @return the full name of the item */ @@ -231,4 +205,31 @@ public class StatisticsUtils { } return translatedItem; } + + private static String translate(String keys, String locale) { + Matcher matcher = CONTENT_PATTERN.matcher(keys); + + StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + String group = matcher.group(); + matcher.appendReplacement(buffer, translateEntry(group.substring(0, group.length() - 1), locale) + ":"); + } + + if (buffer.length() != 0) { + return matcher.appendTail(buffer).toString(); + } + + String[] keySplitted = keys.split(" - "); + for (int i = 0; i < keySplitted.length; i++) { + keySplitted[i] = translateEntry(keySplitted[i], locale); + } + return String.join(" - ", keySplitted); + } + + private static String translateEntry(String key, String locale) { + if (key.startsWith("geyser.")) { + return LanguageUtils.getPlayerLocaleString(key, locale); + } + return LocaleUtils.getLocaleString(key, locale); + } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java index ba008da41..cf9f0e1fb 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java @@ -45,9 +45,8 @@ public class WebUtils { * @return Body contents or error message if the request fails */ public static String getBody(String reqURL) { - URL url = null; try { - url = new URL(reqURL); + URL url = new URL(reqURL); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("GET"); con.setRequestProperty("User-Agent", "Geyser-" + GeyserConnector.getInstance().getPlatformType().toString() + "/" + GeyserConnector.VERSION); // Otherwise Java 8 fails on checking updates diff --git a/connector/src/main/resources/config.yml b/connector/src/main/resources/config.yml index 20728ce2c..79f7bb6bf 100644 --- a/connector/src/main/resources/config.yml +++ b/connector/src/main/resources/config.yml @@ -58,9 +58,9 @@ remote: forward-hostname: false # Floodgate uses encryption to ensure use from authorised sources. -# This should point to the public key generated by Floodgate (Bungee or CraftBukkit) +# This should point to the public key generated by Floodgate (BungeeCord, Spigot or Velocity) # You can ignore this when not using Floodgate. -floodgate-key-file: public-key.pem +floodgate-key-file: key.pem # The Xbox/Minecraft Bedrock username is the key for the Java server auth-info. # This allows automatic configuration/login to the remote Java server. diff --git a/pom.xml b/pom.xml index 68a27d2f2..37faa8515 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.geysermc geyser-parent - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT pom Geyser Allows for players from Minecraft Bedrock Edition to join Minecraft Java Edition servers.