From ec743470121fb67607e73852de141bd1d59ab614 Mon Sep 17 00:00:00 2001 From: TheClashFruit Date: Sat, 11 May 2024 23:08:57 +0200 Subject: [PATCH] feat: very basic ETagging and other stuff I will never do it properly <:trolley:1179808993896575026> --- .../me/theclashfruit/crss/CRSSPlugin.java | 32 ++++++---- .../crss/backend/error/ErrorHandler.java | 36 +++++++++++ .../crss/backend/error/NotFoundServlet.java | 27 -------- .../crss/backend/v2/map/TileServlet.java | 25 ++++++++ .../theclashfruit/crss/models/tile/Tile.java | 64 +++++++++++++++++++ .../crss/models/tile/TileCache.java | 21 ++++++ .../theclashfruit/crss/utils/DataUtils.java | 4 +- .../crss/utils/ReqMiddleware.java | 23 +++++++ .../theclashfruit/crss/utils/UnminedCLI.java | 49 ++++++++------ src/main/resources/config.yml | 3 + src/main/resources/plugin.yml | 8 ++- 11 files changed, 230 insertions(+), 62 deletions(-) create mode 100644 src/main/java/me/theclashfruit/crss/backend/error/ErrorHandler.java delete mode 100644 src/main/java/me/theclashfruit/crss/backend/error/NotFoundServlet.java create mode 100644 src/main/java/me/theclashfruit/crss/models/tile/Tile.java create mode 100644 src/main/java/me/theclashfruit/crss/models/tile/TileCache.java create mode 100644 src/main/java/me/theclashfruit/crss/utils/ReqMiddleware.java diff --git a/src/main/java/me/theclashfruit/crss/CRSSPlugin.java b/src/main/java/me/theclashfruit/crss/CRSSPlugin.java index 8a92840..499f053 100644 --- a/src/main/java/me/theclashfruit/crss/CRSSPlugin.java +++ b/src/main/java/me/theclashfruit/crss/CRSSPlugin.java @@ -1,28 +1,32 @@ package me.theclashfruit.crss; +import me.theclashfruit.crss.backend.error.ErrorHandler; import me.theclashfruit.crss.backend.error.GoneServlet; import me.theclashfruit.crss.backend.IndexServlet; -import me.theclashfruit.crss.backend.error.NotFoundServlet; import me.theclashfruit.crss.backend.v2.map.MarkersServlet; import me.theclashfruit.crss.backend.v2.map.TileServlet; import me.theclashfruit.crss.backend.v2.PlayersServlet; import me.theclashfruit.crss.backend.v2.StatusServlet; import me.theclashfruit.crss.backend.v2.gateway.GatewayServlet; import me.theclashfruit.crss.listeners.PlayerListener; +import me.theclashfruit.crss.utils.ReqMiddleware; +import org.bukkit.Bukkit; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.*; import org.eclipse.jetty.util.Jetty; +import java.util.logging.Logger; + public class CRSSPlugin extends JavaPlugin { Server server = new Server(getConfig().getInt("web.port")); Connector connector = new ServerConnector(server); ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + public static Logger LOGGER = Bukkit.getLogger(); + @Override public void onEnable() { getLogger().info("Plugin enabled!"); @@ -37,13 +41,12 @@ public class CRSSPlugin extends JavaPlugin { context.setContextPath("/api"); // slash - context.addServlet(IndexServlet.class, "/"); + context.addServlet(IndexServlet.class, ""); - // errors + // v1 context.addServlet(GoneServlet.class, "/v1/*"); - context.addServlet(NotFoundServlet.class, "/*"); - // v2 api + // v2 context.addServlet(StatusServlet.class, "/v2/status"); context.addServlet(PlayersServlet.class, "/v2/players"); context.addServlet(GatewayServlet.class, "/v2/gateway"); @@ -54,13 +57,20 @@ public class CRSSPlugin extends JavaPlugin { context.addServlet(MarkersServlet.class, "/v2/map/markers"); } + // websocket JettyWebSocketServletContainerInitializer.configure(context, null); try { - server.setHandler(context); - server.addConnector(connector); + server.setServerInfo("crss/" + getDescription().getVersion() + "+jetty" + Jetty.VERSION); - server.setServerInfo("CRSS/" + getDescription().getVersion() + "+jetty" + Jetty.VERSION); + ReqMiddleware reqMiddleware = new ReqMiddleware(); + + reqMiddleware.setHandler(context); + + server.setHandler(reqMiddleware); + server.setErrorHandler(new ErrorHandler()); + + server.addConnector(connector); if (getConfig().getBoolean("web.enabled") && getConfig().getBoolean("web.api.enabled")) { server.start(); diff --git a/src/main/java/me/theclashfruit/crss/backend/error/ErrorHandler.java b/src/main/java/me/theclashfruit/crss/backend/error/ErrorHandler.java new file mode 100644 index 0000000..f1524d2 --- /dev/null +++ b/src/main/java/me/theclashfruit/crss/backend/error/ErrorHandler.java @@ -0,0 +1,36 @@ +package me.theclashfruit.crss.backend.error; + +import me.theclashfruit.crss.models.ErrorResponse; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.eclipse.jetty.server.handler.ErrorHandler.ERROR_MESSAGE; + +public class ErrorHandler implements Request.Handler { + @Override + public boolean handle(Request req, Response res, Callback callback) throws Exception { + int status = res.getStatus(); + + MimeTypes.Type type = MimeTypes.Type.APPLICATION_JSON; + + res.getHeaders().put(type.getContentTypeField(StandardCharsets.UTF_8)); + + String message = (String) req.getAttribute(ERROR_MESSAGE); + + ErrorResponse error = new ErrorResponse( + status, + status == 404 ? "This endpoint does not exist." : (message != null ? message : "An error occurred.") + ); + + String resJson = error.toJson(); + + res.write(true, ByteBuffer.wrap(resJson.getBytes()), callback); + + return true; + } +} diff --git a/src/main/java/me/theclashfruit/crss/backend/error/NotFoundServlet.java b/src/main/java/me/theclashfruit/crss/backend/error/NotFoundServlet.java deleted file mode 100644 index a7dac35..0000000 --- a/src/main/java/me/theclashfruit/crss/backend/error/NotFoundServlet.java +++ /dev/null @@ -1,27 +0,0 @@ -package me.theclashfruit.crss.backend.error; - -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import me.theclashfruit.crss.models.ErrorResponse; - -import java.io.IOException; - -@WebServlet(name = "API Endpoint Not Found") -public class NotFoundServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { - res.setContentType("application/json"); - - ErrorResponse error = new ErrorResponse( - 404, - "This endpoint does not exist." - ); - - String resJson = error.toJson(); - - res.setStatus(404); - res.getWriter().println(resJson); - } -} diff --git a/src/main/java/me/theclashfruit/crss/backend/v2/map/TileServlet.java b/src/main/java/me/theclashfruit/crss/backend/v2/map/TileServlet.java index 12777b2..d4a37a1 100644 --- a/src/main/java/me/theclashfruit/crss/backend/v2/map/TileServlet.java +++ b/src/main/java/me/theclashfruit/crss/backend/v2/map/TileServlet.java @@ -7,6 +7,9 @@ import jakarta.servlet.http.HttpServletResponse; import me.theclashfruit.crss.utils.UnminedCLI; import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; @WebServlet(name = "Tile API") public class TileServlet extends HttpServlet { @@ -25,8 +28,20 @@ public class TileServlet extends HttpServlet { UnminedCLI unminedCLI = new UnminedCLI(); try { + String eTag = req.getHeader("If-None-Match"); + byte[] tile = unminedCLI.renderTile(iZoom, iX, iY, world); + String tTag = generateETag(tile); + + res.setHeader("ETag", tTag); + + if (eTag != null && eTag.equals(tTag)) { + res.setStatus(304); + + return; + } + res.setStatus(200); res.setContentType("image/png"); @@ -38,4 +53,14 @@ public class TileServlet extends HttpServlet { res.getWriter().println("Internal Server Error"); } } + + public static String generateETag(byte[] data) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(data); + return Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to find SHA-256 algorithm", e); + } + } } diff --git a/src/main/java/me/theclashfruit/crss/models/tile/Tile.java b/src/main/java/me/theclashfruit/crss/models/tile/Tile.java new file mode 100644 index 0000000..2bb966f --- /dev/null +++ b/src/main/java/me/theclashfruit/crss/models/tile/Tile.java @@ -0,0 +1,64 @@ +package me.theclashfruit.crss.models.tile; + +import me.theclashfruit.crss.models.Model; + +public class Tile extends Model { + private String eTag; + + private Integer zoom; + + private Integer x; + private Integer z; + + private String world; + + private Integer timestamp; + + public Tile(String eTag, Integer zoom, Integer x, Integer z, String world) { + this.eTag = eTag; + this.zoom = zoom; + this.x = x; + this.z = z; + this.world = world; + } + + public String getETag() { + return eTag; + } + + public Integer getZoom() { + return zoom; + } + + public Integer getX() { + return x; + } + + public Integer getZ() { + return z; + } + + public String getWorld() { + return world; + } + + public void setETag(String eTag) { + this.eTag = eTag; + } + + public void setZoom(Integer zoom) { + this.zoom = zoom; + } + + public void setX(Integer x) { + this.x = x; + } + + public void setZ(Integer z) { + this.z = z; + } + + public void setWorld(String world) { + this.world = world; + } +} \ No newline at end of file diff --git a/src/main/java/me/theclashfruit/crss/models/tile/TileCache.java b/src/main/java/me/theclashfruit/crss/models/tile/TileCache.java new file mode 100644 index 0000000..a633ff8 --- /dev/null +++ b/src/main/java/me/theclashfruit/crss/models/tile/TileCache.java @@ -0,0 +1,21 @@ +package me.theclashfruit.crss.models.tile; + +import me.theclashfruit.crss.models.Model; + +import java.util.ArrayList; + +public class TileCache extends Model { + private ArrayList tiles; + + public TileCache(ArrayList tiles) { + this.tiles = tiles; + } + + public ArrayList getTiles() { + return tiles; + } + + public void setTiles(ArrayList tiles) { + this.tiles = tiles; + } +} \ No newline at end of file diff --git a/src/main/java/me/theclashfruit/crss/utils/DataUtils.java b/src/main/java/me/theclashfruit/crss/utils/DataUtils.java index a28d132..8260ea8 100644 --- a/src/main/java/me/theclashfruit/crss/utils/DataUtils.java +++ b/src/main/java/me/theclashfruit/crss/utils/DataUtils.java @@ -25,7 +25,7 @@ public class DataUtils { } try (FileReader fileReader = new FileReader(markersFile)) { - return new Gson().fromJson(fileReader, Markers.class); + return JsonUtil.gson.fromJson(fileReader, Markers.class); } } @@ -44,7 +44,7 @@ public class DataUtils { } try (FileWriter fileWriter = new FileWriter(markersFile, false)) { - fileWriter.write(new Gson().toJson(markers)); + fileWriter.write(JsonUtil.gson.toJson(markers)); } } } diff --git a/src/main/java/me/theclashfruit/crss/utils/ReqMiddleware.java b/src/main/java/me/theclashfruit/crss/utils/ReqMiddleware.java new file mode 100644 index 0000000..97aff13 --- /dev/null +++ b/src/main/java/me/theclashfruit/crss/utils/ReqMiddleware.java @@ -0,0 +1,23 @@ +package me.theclashfruit.crss.utils; + +import org.bukkit.Bukkit; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Jetty; + +import static me.theclashfruit.crss.CRSSPlugin.LOGGER; + +public class ReqMiddleware extends ContextHandler { + @Override + public boolean handle(Request req, Response res, Callback callback) throws Exception { + req.setAttribute("me.theclashfruit.crss.currentTimeMillis", System.currentTimeMillis()); + + res.getHeaders().put("X-Powered-By", "Minecraft " + Bukkit.getBukkitVersion()); + res.getHeaders().put("Access-Control-Allow-Origin", "*"); + + return super.handle(req, res, callback); + } +} diff --git a/src/main/java/me/theclashfruit/crss/utils/UnminedCLI.java b/src/main/java/me/theclashfruit/crss/utils/UnminedCLI.java index 691d66d..632c2c9 100644 --- a/src/main/java/me/theclashfruit/crss/utils/UnminedCLI.java +++ b/src/main/java/me/theclashfruit/crss/utils/UnminedCLI.java @@ -1,5 +1,7 @@ package me.theclashfruit.crss.utils; +import com.google.gson.Gson; +import me.theclashfruit.crss.models.tile.TileCache; import org.apache.commons.lang.SystemUtils; import org.bukkit.Bukkit; import org.bukkit.World; @@ -11,7 +13,14 @@ import java.awt.image.RenderedImage; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; +import java.util.concurrent.Semaphore; + +import static me.theclashfruit.crss.CRSSPlugin.LOGGER; public class UnminedCLI { public static Path executablePath = @@ -24,9 +33,15 @@ public class UnminedCLI { .resolve("unmined") .resolve(SystemUtils.IS_OS_WINDOWS ? "unmined-cli.exe" : "unmined-cli"); + private static final Semaphore semaphore = new Semaphore(1); + + private static ArrayList> tilesBeingRendered = new ArrayList<>(); + public UnminedCLI() { } public byte[] renderTile(int zoom, int chunkX, int chunkZ, String world) throws InterruptedException { + semaphore.acquire(); + String safeWorld = world.replaceAll("[^a-zA-Z0-9_\\-]", ""); String[] bukkitWorlds = Bukkit.getWorlds().stream().map(World::getName).toArray(String[]::new); @@ -40,7 +55,7 @@ public class UnminedCLI { if (chunkX > 32767 || chunkZ > 32767 || chunkX < -32767 || chunkZ < -32767) return createErrorImage("Out of bounds!"); else if (zoom < 0) - size = (16 * (zoom * -4)); + size = (16 * (zoom * -2)); else if (zoom < 3 && zoom > 0) size = (16 / (zoom * 2)); else @@ -80,15 +95,13 @@ public class UnminedCLI { tilesPath.toFile().mkdirs(); Thread thread = new Thread(() -> { - Bukkit.getLogger().info("thread started"); - Process proc = null; try { - Bukkit.getLogger().info("try!"); - ProcessBuilder processBuilder = new ProcessBuilder(); + tilesBeingRendered.add(new ArrayList<>(Arrays.asList(chunkX * size, chunkZ * size))); + processBuilder.command( executablePath.toString(), "image", @@ -141,22 +154,22 @@ public class UnminedCLI { proc.waitFor(); } catch (IOException | InterruptedException e) { - Bukkit - .getLogger() - .throwing(this.getClass().getName(), "renderTile", e); - }finally { + LOGGER.throwing(this.getClass().getName(), "renderTile", e); + } finally { if (proc != null) { proc.destroyForcibly(); } } }); + semaphore.release(); + File tileFile = tilesPath .resolve(chunkX + "." + chunkZ + ".png") .toFile(); synchronized (this) { - if (!tileFile.exists()) { + if (!tileFile.exists() && !tilesBeingRendered.contains(new ArrayList<>(Arrays.asList(chunkX * size, chunkZ * size)))) { thread.start(); try { @@ -164,9 +177,9 @@ public class UnminedCLI { } catch (InterruptedException e) { Thread.currentThread().interrupt(); - Bukkit - .getLogger() - .throwing(this.getClass().getName(), "renderTile", e); + LOGGER.throwing(this.getClass().getName(), "renderTile", e); + } finally { + tilesBeingRendered.remove(new ArrayList<>(Arrays.asList(chunkX * size, chunkZ * size))); } byte[] img = readTileImage(tileFile); @@ -182,14 +195,10 @@ public class UnminedCLI { } private byte[] readTileImage(File tileFile) { - byte[] bytes = new byte[(int) tileFile.length()]; - try { return Files.readAllBytes(tileFile.toPath()); } catch (IOException e) { - Bukkit - .getLogger() - .throwing(this.getClass().getName(), "renderTile", e); + LOGGER.throwing(this.getClass().getName(), "renderTile", e); } return null; @@ -211,9 +220,7 @@ public class UnminedCLI { try { ImageIO.write(image, "png", outputStream); } catch (IOException e) { - Bukkit - .getLogger() - .throwing(this.getClass().getName(), "renderTile", e); + LOGGER.throwing(this.getClass().getName(), "renderTile", e); } return outputStream.toByteArray(); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7d65a14..fe2377a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -7,6 +7,9 @@ web: enabled: true map: enabled: true + access_log: + enabled: true + token: 'changeme' gameRules: playerSleepingPercentage: 100 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index a03235b..951b1c2 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,4 +5,10 @@ database: true author: TheClashFruit -load: POSTWORLD \ No newline at end of file +load: POSTWORLD + +commands: + crss: + description: Your description + usage: / + permission: crss.admin \ No newline at end of file