feat: very basic ETagging and other stuff

I will never do it properly <:trolley:1179808993896575026>
This commit is contained in:
TheClashFruit 2024-05-11 23:08:57 +02:00
parent b5a03a71e2
commit ec74347012
Signed by: TheClashFruit
GPG key ID: 09BB24C34C2F3204
11 changed files with 230 additions and 62 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Tile> tiles;
public TileCache(ArrayList<Tile> tiles) {
this.tiles = tiles;
}
public ArrayList<Tile> getTiles() {
return tiles;
}
public void setTiles(ArrayList<Tile> tiles) {
this.tiles = tiles;
}
}

View file

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

View file

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

View file

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

View file

@ -7,6 +7,9 @@ web:
enabled: true
map:
enabled: true
access_log:
enabled: true
token: 'changeme'
gameRules:
playerSleepingPercentage: 100

View file

@ -5,4 +5,10 @@ database: true
author: TheClashFruit
load: POSTWORLD
load: POSTWORLD
commands:
crss:
description: Your description
usage: /<command>
permission: crss.admin