diff --git a/api/src/main/java/com/viaversion/viaversion/api/ViaAPI.java b/api/src/main/java/com/viaversion/viaversion/api/ViaAPI.java index 282282389..a3eb3e816 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/ViaAPI.java +++ b/api/src/main/java/com/viaversion/viaversion/api/ViaAPI.java @@ -63,7 +63,7 @@ public interface ViaAPI { * @return API version incremented with meaningful API changes */ default int apiVersion() { - return 19; + return 20; } /** diff --git a/common/src/main/java/com/viaversion/viaversion/commands/defaultsubs/DumpSubCmd.java b/common/src/main/java/com/viaversion/viaversion/commands/defaultsubs/DumpSubCmd.java index 1a207532d..9bc393f7b 100644 --- a/common/src/main/java/com/viaversion/viaversion/commands/defaultsubs/DumpSubCmd.java +++ b/common/src/main/java/com/viaversion/viaversion/commands/defaultsubs/DumpSubCmd.java @@ -17,32 +17,9 @@ */ package com.viaversion.viaversion.commands.defaultsubs; -import com.google.common.io.CharStreams; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.viaversion.viaversion.api.Via; import com.viaversion.viaversion.api.command.ViaCommandSender; import com.viaversion.viaversion.api.command.ViaSubCommand; -import com.viaversion.viaversion.api.connection.UserConnection; -import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; -import com.viaversion.viaversion.dump.DumpTemplate; -import com.viaversion.viaversion.dump.VersionInfo; -import com.viaversion.viaversion.util.GsonUtil; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.InvalidObjectException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.UUID; -import java.util.logging.Level; +import com.viaversion.viaversion.util.DumpUtil; public class DumpSubCmd extends ViaSubCommand { @@ -57,124 +34,15 @@ public class DumpSubCmd extends ViaSubCommand { } @Override - public boolean execute(ViaCommandSender sender, String[] args) { - VersionInfo version = new VersionInfo( - System.getProperty("java.version"), - System.getProperty("os.name"), - Via.getAPI().getServerVersion().lowestSupportedVersion(), - Via.getManager().getProtocolManager().getSupportedVersions(), - Via.getPlatform().getPlatformName(), - Via.getPlatform().getPlatformVersion(), - Via.getPlatform().getPluginVersion(), - "$IMPL_VERSION", - Via.getManager().getSubPlatforms() - ); - - Map configuration = Via.getPlatform().getConfigurationProvider().getValues(); - DumpTemplate template = new DumpTemplate(version, configuration, Via.getPlatform().getDump(), Via.getManager().getInjector().getDump(), getPlayerSample(sender.getUUID())); - - Via.getPlatform().runAsync(new Runnable() { - @Override - public void run() { - - HttpURLConnection con; - try { - con = (HttpURLConnection) new URL("https://dump.viaversion.com/documents").openConnection(); - } catch (IOException e) { - sender.sendMessage("§4Failed to dump, please check the console for more information"); - Via.getPlatform().getLogger().log(Level.WARNING, "Could not paste ViaVersion dump to ViaVersion Dump", e); - return; - } - try { - con.setRequestProperty("Content-Type", "application/json"); - con.addRequestProperty("User-Agent", "ViaVersion/" + version.getPluginVersion()); - con.setRequestMethod("POST"); - con.setDoOutput(true); - - OutputStream out = con.getOutputStream(); - out.write(new GsonBuilder().setPrettyPrinting().create().toJson(template).getBytes(StandardCharsets.UTF_8)); - out.close(); - - if (con.getResponseCode() == 429) { - sender.sendMessage("§4You can only paste once every minute to protect our systems."); - return; - } - - String rawOutput = CharStreams.toString(new InputStreamReader(con.getInputStream())); - con.getInputStream().close(); - - JsonObject output = GsonUtil.getGson().fromJson(rawOutput, JsonObject.class); - - if (!output.has("key")) - throw new InvalidObjectException("Key is not given in Hastebin output"); - - sender.sendMessage("§2We've made a dump with useful information, report your issue and provide this url: " + getUrl(output.get("key").getAsString())); - } catch (Exception e) { - sender.sendMessage("§4Failed to dump, please check the console for more information"); - Via.getPlatform().getLogger().log(Level.WARNING, "Could not paste ViaVersion dump to Hastebin", e); - try { - if (con.getResponseCode() < 200 || con.getResponseCode() > 400) { - String rawOutput = CharStreams.toString(new InputStreamReader(con.getErrorStream())); - con.getErrorStream().close(); - Via.getPlatform().getLogger().log(Level.WARNING, "Page returned: " + rawOutput); - } - } catch (IOException e1) { - Via.getPlatform().getLogger().log(Level.WARNING, "Failed to capture further info", e1); - } - } + public boolean execute(final ViaCommandSender sender, final String[] args) { + DumpUtil.postDump(sender.getUUID()).whenComplete((url, e) -> { + if (e != null) { + sender.sendMessage("§4" + e.getMessage()); + return; } - }); + sender.sendMessage("§2We've made a dump with useful information, report your issue and provide this url: " + url); + }); return true; } - - private String getUrl(String id) { - return String.format("https://dump.viaversion.com/%s", id); - } - - private JsonObject getPlayerSample(UUID senderUuid) { - JsonObject playerSample = new JsonObject(); - // Player versions - JsonObject versions = new JsonObject(); - playerSample.add("versions", versions); - Map playerVersions = new TreeMap<>((o1, o2) -> ProtocolVersion.getIndex(o2) - ProtocolVersion.getIndex(o1)); - for (UserConnection connection : Via.getManager().getConnectionManager().getConnections()) { - ProtocolVersion protocolVersion = ProtocolVersion.getProtocol(connection.getProtocolInfo().getProtocolVersion()); - playerVersions.compute(protocolVersion, (v, num) -> num != null ? num + 1 : 1); - } - for (Map.Entry entry : playerVersions.entrySet()) { - versions.addProperty(entry.getKey().getName(), entry.getValue()); - } - - // Pipeline of sender - Set> pipelines = new HashSet<>(); - UserConnection senderConnection = Via.getAPI().getConnection(senderUuid); - if (senderConnection != null && senderConnection.getChannel() != null) { - pipelines.add(senderConnection.getChannel().pipeline().names()); - } - - // Other pipelines if different ones are found (3 max) - for (UserConnection connection : Via.getManager().getConnectionManager().getConnections()) { - if (connection.getChannel() == null) { - continue; - } - - List names = connection.getChannel().pipeline().names(); - if (pipelines.add(names) && pipelines.size() == 3) { - break; - } - } - - int i = 0; - for (List pipeline : pipelines) { - JsonArray senderPipeline = new JsonArray(pipeline.size()); - for (String name : pipeline) { - senderPipeline.add(name); - } - - playerSample.add("pipeline-" + i++, senderPipeline); - } - - return playerSample; - } } diff --git a/common/src/main/java/com/viaversion/viaversion/util/DumpUtil.java b/common/src/main/java/com/viaversion/viaversion/util/DumpUtil.java new file mode 100644 index 000000000..6a2612e4a --- /dev/null +++ b/common/src/main/java/com/viaversion/viaversion/util/DumpUtil.java @@ -0,0 +1,215 @@ +/* + * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion + * Copyright (C) 2023 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viaversion.util; + +import com.google.common.io.CharStreams; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.viaversion.viaversion.api.Via; +import com.viaversion.viaversion.api.connection.UserConnection; +import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; +import com.viaversion.viaversion.dump.DumpTemplate; +import com.viaversion.viaversion.dump.VersionInfo; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InvalidObjectException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class DumpUtil { + + /** + * Creates a platform dump and posts it to ViaVersion's dump server asychronously. + * May complete exceptionally with {@link DumpException}. + * + * @param playerToSample uuid of the player to include the pipeline of + * @return completable future that completes with the url of the dump + */ + public static CompletableFuture postDump(@Nullable final UUID playerToSample) { + final VersionInfo version = new VersionInfo( + System.getProperty("java.version"), + System.getProperty("os.name"), + Via.getAPI().getServerVersion().lowestSupportedVersion(), + Via.getManager().getProtocolManager().getSupportedVersions(), + Via.getPlatform().getPlatformName(), + Via.getPlatform().getPlatformVersion(), + Via.getPlatform().getPluginVersion(), + "$IMPL_VERSION", + Via.getManager().getSubPlatforms() + ); + final Map configuration = Via.getPlatform().getConfigurationProvider().getValues(); + final DumpTemplate template = new DumpTemplate(version, configuration, Via.getPlatform().getDump(), Via.getManager().getInjector().getDump(), getPlayerSample(playerToSample)); + final CompletableFuture result = new CompletableFuture<>(); + Via.getPlatform().runAsync(() -> { + final HttpURLConnection con; + try { + con = (HttpURLConnection) new URL("https://dump.viaversion.com/documents").openConnection(); + } catch (final IOException e) { + Via.getPlatform().getLogger().log(Level.SEVERE, "Error when opening connection to ViaVersion dump service", e); + result.completeExceptionally(new DumpException(DumpErrorType.CONNECTION, e)); + return; + } + + try { + con.setRequestProperty("Content-Type", "application/json"); + con.addRequestProperty("User-Agent", "ViaVersion-" + Via.getPlatform().getPlatformName() + "/" + version.getPluginVersion()); + con.setRequestMethod("POST"); + con.setDoOutput(true); + + try (final OutputStream out = con.getOutputStream()) { + out.write(new GsonBuilder().setPrettyPrinting().create().toJson(template).getBytes(StandardCharsets.UTF_8)); + } + + if (con.getResponseCode() == 429) { + result.completeExceptionally(new DumpException(DumpErrorType.RATE_LIMITED)); + return; + } + + final String rawOutput; + try (final InputStream inputStream = con.getInputStream()) { + rawOutput = CharStreams.toString(new InputStreamReader(inputStream)); + } + + final JsonObject output = GsonUtil.getGson().fromJson(rawOutput, JsonObject.class); + if (!output.has("key")) { + throw new InvalidObjectException("Key is not given in Hastebin output"); + } + + result.complete(urlForId(output.get("key").getAsString())); + } catch (final Exception e) { + Via.getPlatform().getLogger().log(Level.SEVERE, "Error when posting ViaVersion dump", e); + result.completeExceptionally(new DumpException(DumpErrorType.POST, e)); + printFailureInfo(con); + } + }); + return result; + } + + private static void printFailureInfo(final HttpURLConnection connection) { + try { + if (connection.getResponseCode() < 200 || connection.getResponseCode() > 400) { + try (final InputStream errorStream = connection.getErrorStream()) { + final String rawOutput = CharStreams.toString(new InputStreamReader(errorStream)); + Via.getPlatform().getLogger().log(Level.SEVERE, "Page returned: " + rawOutput); + } + } + } catch (final IOException e) { + Via.getPlatform().getLogger().log(Level.SEVERE, "Failed to capture further info", e); + } + } + + public static String urlForId(final String id) { + return String.format("https://dump.viaversion.com/%s", id); + } + + private static JsonObject getPlayerSample(@Nullable final UUID uuid) { + final JsonObject playerSample = new JsonObject(); + // Player versions + final JsonObject versions = new JsonObject(); + playerSample.add("versions", versions); + final Map playerVersions = new TreeMap<>((o1, o2) -> ProtocolVersion.getIndex(o2) - ProtocolVersion.getIndex(o1)); + for (final UserConnection connection : Via.getManager().getConnectionManager().getConnections()) { + final ProtocolVersion protocolVersion = ProtocolVersion.getProtocol(connection.getProtocolInfo().getProtocolVersion()); + playerVersions.compute(protocolVersion, (v, num) -> num != null ? num + 1 : 1); + } + for (final Map.Entry entry : playerVersions.entrySet()) { + versions.addProperty(entry.getKey().getName(), entry.getValue()); + } + + final Set> pipelines = new HashSet<>(); + if (uuid != null) { + // Pipeline of sender + final UserConnection senderConnection = Via.getAPI().getConnection(uuid); + if (senderConnection != null && senderConnection.getChannel() != null) { + pipelines.add(senderConnection.getChannel().pipeline().names()); + } + } + + // Other pipelines if different ones are found (3 max) + for (final UserConnection connection : Via.getManager().getConnectionManager().getConnections()) { + if (connection.getChannel() == null) { + continue; + } + + final List names = connection.getChannel().pipeline().names(); + if (pipelines.add(names) && pipelines.size() == 3) { + break; + } + } + + int i = 0; + for (final List pipeline : pipelines) { + final JsonArray senderPipeline = new JsonArray(pipeline.size()); + for (final String name : pipeline) { + senderPipeline.add(name); + } + + playerSample.add("pipeline-" + i++, senderPipeline); + } + + return playerSample; + } + + public static final class DumpException extends RuntimeException { + private final DumpErrorType errorType; + + private DumpException(final DumpErrorType errorType, final Throwable cause) { + super(errorType.message(), cause); + this.errorType = errorType; + } + + private DumpException(final DumpErrorType errorType) { + super(errorType.message()); + this.errorType = errorType; + } + + public DumpErrorType errorType() { + return errorType; + } + } + + public enum DumpErrorType { + + CONNECTION("Failed to dump, please check the console for more information"), + RATE_LIMITED("Please wait before creating another dump"), + POST("Failed to dump, please check the console for more information"); + + private final String message; + + DumpErrorType(final String message) { + this.message = message; + } + + public String message() { + return message; + } + } +}