feat: very basic ETagging and other stuff
I will never do it properly <:trolley:1179808993896575026>
This commit is contained in:
parent
b5a03a71e2
commit
ec74347012
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
64
src/main/java/me/theclashfruit/crss/models/tile/Tile.java
Normal file
64
src/main/java/me/theclashfruit/crss/models/tile/Tile.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
23
src/main/java/me/theclashfruit/crss/utils/ReqMiddleware.java
Normal file
23
src/main/java/me/theclashfruit/crss/utils/ReqMiddleware.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -7,6 +7,9 @@ web:
|
|||
enabled: true
|
||||
map:
|
||||
enabled: true
|
||||
access_log:
|
||||
enabled: true
|
||||
token: 'changeme'
|
||||
|
||||
gameRules:
|
||||
playerSleepingPercentage: 100
|
|
@ -6,3 +6,9 @@ database: true
|
|||
author: TheClashFruit
|
||||
|
||||
load: POSTWORLD
|
||||
|
||||
commands:
|
||||
crss:
|
||||
description: Your description
|
||||
usage: /<command>
|
||||
permission: crss.admin
|
Reference in a new issue