From a2618233029b0bf381b5f6e937ab91d59951fb7e Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Wed, 8 Aug 2018 04:44:27 -0400 Subject: [PATCH] Add favicon support --- .gitignore | 3 +- .../velocitypowered/api/server/Favicon.java | 88 +++++++++++++++++++ .../com/velocitypowered/proxy/Velocity.java | 6 ++ .../velocitypowered/proxy/VelocityServer.java | 3 + .../proxy/config/VelocityConfiguration.java | 25 ++++++ .../client/StatusSessionHandler.java | 2 +- .../proxy/data/ServerPing.java | 7 +- .../protocol/util/FaviconSerializer.java | 18 ++++ 8 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/com/velocitypowered/api/server/Favicon.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java diff --git a/.gitignore b/.gitignore index 7ae951cf7..0e54c3ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,5 @@ gradle-app.setting # Other trash logs/ -/velocity.toml \ No newline at end of file +/velocity.toml +server-icon.png \ No newline at end of file diff --git a/api/src/main/java/com/velocitypowered/api/server/Favicon.java b/api/src/main/java/com/velocitypowered/api/server/Favicon.java new file mode 100644 index 000000000..ceeaf517c --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/server/Favicon.java @@ -0,0 +1,88 @@ +package com.velocitypowered.api.server; + +import com.google.common.base.Preconditions; + +import javax.annotation.Nonnull; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Objects; + +/** + * Represents a Minecraft server favicon. A Minecraft server favicon is a 64x64 image that can be displayed to a remote + * client that sends a Server List Ping packet, and is automatically displayed in the Minecraft client. + */ +public final class Favicon { + private final String base64Url; + + /** + * Directly create a favicon using its Base64 URL directly. You are generally better served by the create() series + * of functions. + * @param base64Url the url for use with this favicon + */ + public Favicon(@Nonnull String base64Url) { + this.base64Url = Preconditions.checkNotNull(base64Url, "base64Url"); + } + + /** + * Returns the Base64-encoded URI for this image. + * @return a URL representing this favicon + */ + public String getBase64Url() { + return base64Url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Favicon favicon = (Favicon) o; + return Objects.equals(base64Url, favicon.base64Url); + } + + @Override + public int hashCode() { + return Objects.hash(base64Url); + } + + @Override + public String toString() { + return "Favicon{" + + "base64Url='" + base64Url + '\'' + + '}'; + } + + /** + * Creates a new {@code Favicon} from the specified {@code image}. + * @param image the image to use for the favicon + * @return the created {@link Favicon} instance + */ + public static Favicon create(@Nonnull BufferedImage image) { + Preconditions.checkNotNull(image, "image"); + Preconditions.checkArgument(image.getWidth() == 64 && image.getHeight() == 64, "Image does not have" + + " 64x64 dimensions (found %sx%s)", image.getWidth(), image.getHeight()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + ImageIO.write(image, "PNG", os); + } catch (IOException e) { + throw new AssertionError(e); + } + return new Favicon("data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray())); + } + + /** + * Creates a new {@code Favicon} by reading the image from the specified {@code path}. + * @param path the path to the image to create a favicon for + * @return the created {@link Favicon} instance + */ + public static Favicon create(@Nonnull Path path) throws IOException { + try (InputStream stream = Files.newInputStream(path)) { + return create(ImageIO.read(stream)); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java index 6efd6a6c7..e2ee28950 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java @@ -3,6 +3,12 @@ package com.velocitypowered.proxy; import com.velocitypowered.proxy.console.VelocityConsole; public class Velocity { + static { + // We use BufferedImage for favicons, and on macOS this puts the Java application in the dock. How inconvenient. + // Force AWT to work with its head chopped off. + System.setProperty("java.awt.headless", "true"); + } + public static void main(String... args) { final VelocityServer server = VelocityServer.getServer(); server.start(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 3cff9b6bd..e548d54c3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder; import com.velocitypowered.api.command.CommandInvoker; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.server.Favicon; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.network.ConnectionManager; import com.velocitypowered.proxy.command.ServerCommand; @@ -17,6 +18,7 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.http.NettyHttpClient; import com.velocitypowered.api.server.ServerInfo; import com.velocitypowered.proxy.command.CommandManager; +import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.EncryptionUtils; import com.velocitypowered.proxy.util.ServerMap; @@ -44,6 +46,7 @@ public class VelocityServer implements ProxyServer { private static final VelocityServer INSTANCE = new VelocityServer(); public static final Gson GSON = new GsonBuilder() .registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer()) + .registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer()) .create(); private final ConnectionManager cm = new ConnectionManager(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 10f697903..cd5f5536c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -2,6 +2,7 @@ package com.velocitypowered.proxy.config; import com.google.common.collect.ImmutableMap; import com.moandjiezana.toml.Toml; +import com.velocitypowered.api.server.Favicon; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.api.util.LegacyChatColorUtils; import net.kyori.text.Component; @@ -15,6 +16,7 @@ import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,6 +38,7 @@ public class VelocityConfiguration { private final int queryPort; private Component motdAsComponent; + private Favicon favicon; private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode, IPForwardingMode ipForwardingMode, Map servers, @@ -124,9 +127,22 @@ public class VelocityConfiguration { logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance."); } + loadFavicon(); + return valid; } + private void loadFavicon() { + Path faviconPath = Paths.get("server-icon.png"); + if (Files.exists(faviconPath)) { + try { + this.favicon = Favicon.create(faviconPath); + } catch (Exception e) { + logger.info("Unable to load your server-icon.png, continuing without it.", e); + } + } + } + public InetSocketAddress getBind() { return AddressUtil.parseAddress(bind); } @@ -182,6 +198,14 @@ public class VelocityConfiguration { return compressionLevel; } + public Favicon getFavicon() { + return favicon; + } + + public static Logger getLogger() { + return logger; + } + @Override public String toString() { return "VelocityConfiguration{" + @@ -197,6 +221,7 @@ public class VelocityConfiguration { ", queryEnabled=" + queryEnabled + ", queryPort=" + queryPort + ", motdAsComponent=" + motdAsComponent + + ", favicon=" + favicon + '}'; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index e75eafebd..7d5f886bd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -41,7 +41,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler { new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING), new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()), configuration.getMotdComponent(), - null + configuration.getFavicon() ); StatusResponse response = new StatusResponse(); response.setStatus(VelocityServer.GSON.toJson(ping)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java b/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java index 72494eaf1..a6cb2a746 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/data/ServerPing.java @@ -1,14 +1,15 @@ package com.velocitypowered.proxy.data; +import com.velocitypowered.api.server.Favicon; import net.kyori.text.Component; public class ServerPing { private final Version version; private final Players players; private final Component description; - private final String favicon; + private final Favicon favicon; - public ServerPing(Version version, Players players, Component description, String favicon) { + public ServerPing(Version version, Players players, Component description, Favicon favicon) { this.version = version; this.players = players; this.description = description; @@ -27,7 +28,7 @@ public class ServerPing { return description; } - public String getFavicon() { + public Favicon getFavicon() { return favicon; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java new file mode 100644 index 000000000..7243e8b06 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/FaviconSerializer.java @@ -0,0 +1,18 @@ +package com.velocitypowered.proxy.protocol.util; + +import com.google.gson.*; +import com.velocitypowered.api.server.Favicon; + +import java.lang.reflect.Type; + +public class FaviconSerializer implements JsonSerializer, JsonDeserializer { + @Override + public Favicon deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new Favicon(json.getAsString()); + } + + @Override + public JsonElement serialize(Favicon src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getBase64Url()); + } +}