Merge remote-tracking branch 'origin/floodgate-2.0' into feature/1.17

# Conflicts:
#	connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java
This commit is contained in:
Tim203 2021-06-06 01:16:57 +02:00
commit 1ded2086e3
No known key found for this signature in database
GPG Key ID: 064EE9F5BF7C3EE8
84 changed files with 2747 additions and 1868 deletions

5
Jenkinsfile vendored
View File

@ -22,7 +22,10 @@ pipeline {
stage ('Deploy') {
when {
branch "master"
anyOf {
branch "master"
branch "floodgate-2.0"
}
}
steps {

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-bungeecord</artifactId>
@ -14,7 +14,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>connector</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -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;

View File

@ -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");

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-parent</artifactId>
<packaging>pom</packaging>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-spigot</artifactId>
@ -21,7 +21,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>connector</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -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;

View File

@ -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");

View File

@ -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:

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-sponge</artifactId>
@ -14,7 +14,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>connector</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-standalone</artifactId>
@ -14,7 +14,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>connector</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>bootstrap-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>bootstrap-velocity</artifactId>
@ -14,7 +14,7 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>connector</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -6,22 +6,20 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>common</artifactId>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
<scope>compile</scope>
<groupId>org.geysermc.cumulus</groupId>
<artifactId>cumulus</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.8</version>
<scope>compile</scope>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
</dependencies>
</project>

View File

@ -31,7 +31,6 @@ import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PlatformType {
ANDROID("Android"),
BUNGEECORD("BungeeCord"),
FABRIC("Fabric"),

View File

@ -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<FormComponent> content;
public CustomFormWindow(String title) {
this(title, new ArrayList<>());
}
public CustomFormWindow(String title, List<FormComponent> content) {
this(title, content, (FormImage) null);
}
public CustomFormWindow(String title, List<FormComponent> content, String icon) {
this(title, content, new FormImage(FormImage.FormImageType.URL, icon));
}
public CustomFormWindow(String title, List<FormComponent> 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<Integer, FormResponseData> dropdownResponses = new HashMap<Integer, FormResponseData>();
Map<Integer, String> inputResponses = new HashMap<Integer, String>();
Map<Integer, Float> sliderResponses = new HashMap<Integer, Float>();
Map<Integer, FormResponseData> stepSliderResponses = new HashMap<Integer, FormResponseData>();
Map<Integer, Boolean> toggleResponses = new HashMap<Integer, Boolean>();
Map<Integer, Object> responses = new HashMap<Integer, Object>();
Map<Integer, String> labelResponses = new HashMap<Integer, String>();
List<String> componentResponses = new ArrayList<>();
try {
componentResponses = new ObjectMapper().readValue(data.trim(), new TypeReference<List<String>>(){});
} 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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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<FormButton> buttons;
public SimpleFormWindow(String title, String content) {
this(title, content, new ArrayList<FormButton>());
}
public SimpleFormWindow(String title, String content, List<FormButton> 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));
}
}

View File

@ -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<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<String> steps;
@Getter
@Setter
private int defaultStepIndex;
public StepSliderComponent(String text) {
this(text, new ArrayList<String>());
}
public StepSliderComponent(String text, List<String> steps) {
this(text, steps, 0);
}
public StepSliderComponent(String text, List<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<Integer, Object> responses;
private Map<Integer, FormResponseData> dropdownResponses;
private Map<Integer, String> inputResponses;
private Map<Integer, Float> sliderResponses;
private Map<Integer, FormResponseData> stepSliderResponses;
private Map<Integer, Boolean> toggleResponses;
private Map<Integer, String> labelResponses;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}

View File

@ -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.<br> 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.<br> 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.<br> 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;
}
}

View File

@ -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));
}
}

View File

@ -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);
}

View File

@ -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<NewsItemAction> actions;
private final String url;
private NewsItem(int id, String project, boolean active, NewsType type, ItemData data,
boolean priority, String message, Set<NewsItemAction> 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<NewsItemAction> 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 extends ItemData> T getDataAs(Class<T> 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<NewsItemAction> getActions() {
return actions;
}
public String getUrl() {
return url;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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<JsonObject, ? extends ItemData> readFunction;
NewsType(Function<JsonObject, ? extends ItemData> 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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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];
}

View File

@ -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 extends Key> T getKeyFromFile(Path fileLocation, Class<T> 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)
);
}
}

View File

@ -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;
}

View File

@ -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];
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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];
}
}

View File

@ -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;
}
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
</parent>
<artifactId>connector</artifactId>
@ -20,7 +20,13 @@
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>common</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.10.2</version>
<scope>compile</scope>
</dependency>
<dependency>
@ -29,6 +35,12 @@
<version>2.10.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.CloudburstMC.Protocol</groupId>
<artifactId>bedrock-v440</artifactId>
@ -206,11 +218,13 @@
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.11</version> <!-- This isn't the latest version to get round https://github.com/ronmamo/reflections/issues/273 -->
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
@ -246,6 +260,7 @@
<groupId>com.github.GeyserMC</groupId>
<artifactId>MCAuthLib</artifactId>
<version>0e48a094f2</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@ -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;

View File

@ -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.

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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<DeviceOS> userPlatforms;
private final Object2IntMap<DeviceOs> 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;

View File

@ -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;
}

View File

@ -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
}

View File

@ -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<TeleportCache> 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.
*
* <p>
* 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();

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<String, GeyserAdvancement> 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<String, GeyserAdvancement> 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<String, GeyserAdvancement> 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<String, GeyserAdvancement> 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) {

View File

@ -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<Form> 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<String> responseConsumer = form.getResponseHandler();
if (responseConsumer != null) {
responseConsumer.accept(response.getFormData());
}
removeWindow(response.getFormId());
}
public boolean removeWindow(int id) {
return forms.remove(id) != null;
}
}

View File

@ -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<FormWindow> 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);
}
}

View File

@ -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<Netwo
@Override
public void translate(NetworkStackLatencyPacket packet, GeyserSession session) {
if (session.getConnector().getConfig().isForwardPlayerPing()) {
long pingId;
// so apparently, as of 1.16.200
// PS4 divides the network stack latency timestamp FOR US!!!
// WTF
if (session.getClientData().getDeviceOS().equals(DeviceOS.NX)) {
// Ignore the weird DeviceOS, our order is wrong and will be fixed in Floodgate 2.0
pingId = packet.getTimestamp();
} else {
pingId = packet.getTimestamp() / 1000;
}
session.sendDownstreamPacket(new ClientKeepAlivePacket(pingId));
long pingId;
// so apparently, as of 1.16.200
// PS4 divides the network stack latency timestamp FOR US!!!
// WTF
if (session.getClientData().getDeviceOs().equals(DeviceOs.PS4)) {
pingId = packet.getTimestamp();
} else {
pingId = packet.getTimestamp() / 1000;
}
// negative timestamps are used as hack to fix the url image loading bug
if (packet.getTimestamp() > 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);
}
}

View File

@ -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<ServerSettingsRequestPacket> {
@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);
}

View File

@ -36,10 +36,10 @@ import org.geysermc.connector.network.translators.Translator;
*/
@Translator(packet = ServerAdvancementTabPacket.class)
public class JavaAdvancementsTabTranslator extends PacketTranslator<ServerAdvancementTabPacket> {
@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();
}
}

View File

@ -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<ServerJoinGamePacke
session.sendDownstreamPacket(new ClientPluginMessagePacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));
// register the plugin messaging channels used in Floodgate
if (session.getConnector().getDefaultAuthType() == AuthType.FLOODGATE) {
session.sendDownstreamPacket(new ClientPluginMessagePacket("minecraft:register", PluginMessageUtils.getFloodgateRegisterData()));
}
if (!newDimension.equals(session.getDimension())) {
DimensionUtils.switchDimension(session, newDimension);
}

View File

@ -0,0 +1,80 @@
/*
* 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.translators.java;
import com.github.steveice10.mc.protocol.packet.ingame.client.ClientPluginMessagePacket;
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerPluginMessagePacket;
import com.google.common.base.Charsets;
import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.cumulus.Form;
import org.geysermc.cumulus.Forms;
import org.geysermc.cumulus.util.FormType;
import java.nio.charset.StandardCharsets;
@Translator(packet = ServerPluginMessagePacket.class)
public class JavaPluginMessageTranslator extends PacketTranslator<ServerPluginMessagePacket> {
@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);
}
}
}

View File

@ -40,7 +40,7 @@ public class JavaStatisticsTranslator extends PacketTranslator<ServerStatisticsP
if (session.isWaitingForStatistics()) {
session.setWaitingForStatistics(false);
session.sendForm(StatisticsUtils.buildMenuForm(session), StatisticsUtils.STATISTICS_MENU_FORM_ID);
StatisticsUtils.buildAndSendStatisticsMenu(session);
}
}
}

View File

@ -0,0 +1,235 @@
/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.skin;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.GeyserLogger;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.Constants;
import org.geysermc.connector.utils.PluginMessageUtils;
import org.geysermc.floodgate.util.WebsocketEventType;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import javax.net.ssl.SSLException;
import java.net.ConnectException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import static org.geysermc.connector.utils.PluginMessageUtils.getSkinChannel;
public final class FloodgateSkinUploader {
private final ObjectMapper JACKSON = new ObjectMapper();
private final List<String> skinQueue = new ArrayList<>();
private final GeyserLogger logger;
private final WebSocketClient client;
@Getter private int id;
@Getter private String verifyCode;
@Getter private int subscribersCount;
public FloodgateSkinUploader(GeyserConnector connector) {
this.logger = connector.getLogger();
this.client = new WebSocketClient(Constants.GLOBAL_API_WS_URI) {
@Override
public void onOpen(ServerHandshake handshake) {
setConnectionLostTimeout(11);
Iterator<String> queueIterator = skinQueue.iterator();
while (isOpen() && queueIterator.hasNext()) {
send(queueIterator.next());
queueIterator.remove();
}
}
@Override
public void onMessage(String message) {
// 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();
}
}

View File

@ -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();
}

View File

@ -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<String> 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;
}
}

View File

@ -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 &&

View File

@ -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()));
}
})
);
}
}

View File

@ -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<Integer, NewsItem> 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<NewsItem> getActiveNews() {
return activeNewsItems.values();
}
public Collection<NewsItem> getActiveNews(NewsItemAction action) {
List<NewsItem> 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();
}
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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<Statistic, Integer> 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);
}
}

View File

@ -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

View File

@ -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.

View File

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.geysermc</groupId>
<artifactId>geyser-parent</artifactId>
<version>1.2.1-SNAPSHOT</version>
<version>1.3.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Geyser</name>
<description>Allows for players from Minecraft Bedrock Edition to join Minecraft Java Edition servers.</description>