diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java index 52aa7468a..08ce75991 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java @@ -5,7 +5,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.io.CharStreams; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.SimpleCommand; import com.velocitypowered.api.permission.Tristate; @@ -17,6 +21,12 @@ import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.util.InformationUtils; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -320,9 +330,18 @@ public class VelocityCommand implements SimpleCommand { servers.add(iter.getServerInfo().getName(), InformationUtils.collectServerInfo(iter)); } + JsonArray connectOrder = new JsonArray(); + List attemptedConnectionOrder = ImmutableList.copyOf( + server.getConfiguration().getAttemptConnectionOrder()); + for (int i = 0; i < attemptedConnectionOrder.size(); i++) { + connectOrder.add(attemptedConnectionOrder.get(i)); + } JsonObject proxyConfig = InformationUtils.collectProxyConfig(server.getConfiguration()); proxyConfig.add("servers", servers); + proxyConfig.add("connectOrder", connectOrder); + proxyConfig.add("forcedHosts", + InformationUtils.collectForcedHosts(server.getConfiguration())); JsonObject dump = new JsonObject(); dump.add("versionInfo", InformationUtils.collectProxyInfo(server.getVersion())); @@ -330,7 +349,90 @@ public class VelocityCommand implements SimpleCommand { dump.add("config", proxyConfig); dump.add("plugins", InformationUtils.collectPluginInfo(server)); - // TODO: Finish + source.sendMessage(Component.text().content("Uploading gathered information...").build()); + + HttpURLConnection upload = null; + try { + upload = (HttpURLConnection) new URL("https://dump.velocitypowered.com/documents") + .openConnection(); + } catch (IOException e1) { + // Couldn't open connection; + source.sendMessage( + Component.text() + .content("Failed to open a connection!") + .color(NamedTextColor.RED).build()); + return; + } + upload.setRequestProperty("Content-Type", "text/plain"); + upload.addRequestProperty( + "User-Agent", server.getVersion().getName() + "/" + + server.getVersion().getVersion()); + try { + upload.setRequestMethod("POST"); + upload.setDoOutput(true); + + OutputStream uploadStream = upload.getOutputStream(); + uploadStream.write( + InformationUtils.toHumanReadableString(dump).getBytes(StandardCharsets.UTF_8)); + uploadStream.close(); + } catch (IOException e2) { + // Couldn't POST the Data + source.sendMessage( + Component.text() + .content("Couldn't upload the data!") + .color(NamedTextColor.RED).build()); + return; + } + String rawResponse = null; + try { + rawResponse = CharStreams.toString( + new InputStreamReader(upload.getInputStream(), StandardCharsets.UTF_8)); + upload.getInputStream().close(); + } catch (IOException e3) { + // Couldn't read response + source.sendMessage( + Component.text() + .content("Invalid server response received!") + .color(NamedTextColor.RED).build()); + } + JsonObject returned = null; + try { + returned = InformationUtils.parseString(rawResponse); + if (returned == null || !returned.has("key")) { + throw new JsonParseException("Invalid json response"); + } + } catch (JsonSyntaxException e4) { + // Mangled json + source.sendMessage( + Component.text() + .content("Server responded with invalid data!") + .color(NamedTextColor.RED).build()); + return; + } catch (JsonParseException e5) { + // Backend error? + source.sendMessage( + Component.text() + .content("Data was uploaded successfully but couldn't be posted") + .color(NamedTextColor.RED).build()); + return; + } + TextComponent response = Component.text() + .content("Created an anonymised report containing useful information about") + .append(Component.newline() + .append( + Component.text("this proxy. If a developer requested it" + + ", you may share the")) + .append(Component.newline()) + .append(Component.text("following link with them:")) + .append(Component.newline()) + .append(Component.text("https://dump.velocitypowered.com/" + + returned.get("key").getAsString() + ".json") + .color(NamedTextColor.GREEN))) + .append(Component.newline()) + .append(Component.text("Note: This link is only valid for a few days")) + .build(); + source.sendMessage(response); + } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java index ebc1dd015..9016ae3ff 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java @@ -1,6 +1,7 @@ package com.velocitypowered.proxy.util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; @@ -16,9 +17,15 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.ProxyVersion; import io.netty.channel.unix.DomainSocketAddress; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.List; +import java.util.Map; + import joptsimple.internal.Strings; public enum InformationUtils { @@ -84,6 +91,75 @@ public enum InformationUtils { return envInfo; } + /** + * Creates a {@link JsonObject} containing information about the + * forced hosts of the {@link ProxyConfig} instance. + * + * @return {@link JsonArray} containing forced hosts + */ + public static JsonObject collectForcedHosts(ProxyConfig config) { + JsonObject forcedHosts = new JsonObject(); + Map> allForcedHosts = ImmutableMap.copyOf( + config.getForcedHosts()); + for (Map.Entry> entry : allForcedHosts.entrySet()) { + JsonArray host = new JsonArray(); + for (int i = 0; i < entry.getValue().size(); i++) { + host.add(entry.getValue().get(i)); + } + forcedHosts.add(entry.getKey(), host); + } + return forcedHosts; + } + + /** + * Anonymises or redacts a given {@link InetAddress} + * public address bits. + * + * @param address The address to redact + * @return {@link String} address with public parts redacted + */ + public static String anonymizeInetAddress(InetAddress address) { + if (address instanceof Inet4Address) { + Inet4Address v4 = (Inet4Address) address; + if (v4.isAnyLocalAddress() || v4.isLoopbackAddress() + || v4.isLinkLocalAddress() + || v4.isSiteLocalAddress()) { + return address.getHostAddress(); + } else { + byte[] addr = v4.getAddress(); + return (addr[0] & 0xff) + "." + (addr[1] & 0xff) + ".XXX.XXX"; + } + } else if (address instanceof Inet6Address) { + Inet6Address v6 = (Inet6Address) address; + if (v6.isAnyLocalAddress() || v6.isLoopbackAddress() + || v6.isSiteLocalAddress() + || v6.isSiteLocalAddress()) { + return address.getHostAddress(); + } else { + String[] bits = v6.getHostAddress().split(":"); + String ret = ""; + boolean flag = false; + for (int iter = 0; iter < bits.length; iter++) { + if (flag) { + ret += ":X"; + continue; + } + if (!bits[iter].equals("0")) { + if (iter == 0) { + ret = bits[iter]; + } else { + ret = "::" + bits[iter]; + } + flag = true; + } + } + return ret; + } + } else { + return address.getHostAddress(); + } + } + /** * Creates a {@link JsonObject} containing most relevant * information of the {@link RegisteredServer} for diagnosis. @@ -97,18 +173,22 @@ public enum InformationUtils { SocketAddress address = server.getServerInfo().getAddress(); if (address instanceof InetSocketAddress) { InetSocketAddress iaddr = (InetSocketAddress) address; - info.addProperty("socketType", "EventLoop"); + info.addProperty("socketType", "EventLoop/NIO"); info.addProperty("unresolved", iaddr.isUnresolved()); - // Greetings form Netty 4aa10db9 - info.addProperty("host", iaddr.getHostString()); + if (iaddr.isUnresolved()) { + // Greetings form Netty 4aa10db9 + info.addProperty("host", iaddr.getHostString()); + } else { + info.addProperty("host", anonymizeInetAddress(iaddr.getAddress())); + } info.addProperty("port", iaddr.getPort()); } else if (address instanceof DomainSocketAddress) { DomainSocketAddress daddr = (DomainSocketAddress) address; info.addProperty("socketType", "Unix/Epoll"); - info.addProperty("host", daddr.path()); + info.addProperty("path", daddr.path()); } else { info.addProperty("socketType", "Unknown/Generic"); - info.addProperty("host", address.toString()); + info.addProperty("info", address.toString()); } return info; } @@ -135,6 +215,26 @@ public enum InformationUtils { return (JsonObject) serializeObject(config, true); } + /** + * Creates a human-readable String from a {@link JsonElement}. + * + * @param json the {@link JsonElement} object + * @return the human-readable String + */ + public static String toHumanReadableString(JsonElement json) { + return GSON_WITHOUT_EXCLUDES.toJson(json); + } + + /** + * Creates a {@link JsonObject} from a String. + * + * @param toParse the String to parse + * @return {@link JsonObject} object + */ + public static JsonObject parseString(String toParse) { + return GSON_WITHOUT_EXCLUDES.fromJson(toParse, JsonObject.class); + } + private static JsonElement serializeObject(Object toSerialize, boolean withExcludes) { return JsonParser.parseString( withExcludes ? GSON_WITH_EXCLUDES.toJson(toSerialize) :