This commit is contained in:
2026-05-19 21:56:26 -04:00
parent e2fb3fb21d
commit 533b5b52b8
29 changed files with 346 additions and 186 deletions
+76 -53
View File
@@ -3,7 +3,6 @@ package dev.plex;
import dev.plex.assets.MinecraftAssetsManager; import dev.plex.assets.MinecraftAssetsManager;
import dev.plex.authentication.AuthenticationManager; import dev.plex.authentication.AuthenticationManager;
import dev.plex.cache.FileCache; import dev.plex.cache.FileCache;
import dev.plex.api.PlexApi;
import dev.plex.config.ModuleConfig; import dev.plex.config.ModuleConfig;
import dev.plex.logging.Log; import dev.plex.logging.Log;
import dev.plex.module.PlexModule; import dev.plex.module.PlexModule;
@@ -33,35 +32,41 @@ import java.util.concurrent.atomic.AtomicReference;
public class HTTPDModule extends PlexModule public class HTTPDModule extends PlexModule
{ {
public static ServletContextHandler context; @Getter
private ServletContextHandler context;
private Thread serverThread; private Thread serverThread;
private AtomicReference<Server> atomicServer = new AtomicReference<>(); private final AtomicReference<Server> atomicServer = new AtomicReference<>();
public static ModuleConfig moduleConfig;
private static PlexApi plexApi;
public static PlexApi plexApi()
{
return plexApi;
}
public static final FileCache fileCache = new FileCache();
public static final String template = AbstractServlet.readFileReal(HTTPDModule.class.getResourceAsStream("/httpd/template.html"));
@Getter @Getter
private static AuthenticationManager authenticationManager; private ModuleConfig moduleConfig;
@Getter @Getter
private static File accessLogFile; private final FileCache fileCache = new FileCache();
@Getter @Getter
private static MinecraftAssetsManager minecraftAssetsManager; private final String template = AbstractServlet.readFileReal(HTTPDModule.class.getResourceAsStream("/httpd/template.html"));
@Getter
private AuthenticationManager authenticationManager;
@Getter
private File accessLogFile;
@Getter
private MinecraftAssetsManager minecraftAssetsManager;
@Getter
private StatsBroadcaster statsBroadcaster;
@Getter
private PlayersBroadcaster playersBroadcaster;
@Getter
private PlayerInventoryBroadcaster playerInventoryBroadcaster;
@Override @Override
public void load() public void load()
{ {
plexApi = api();
// Move it from /httpd/config.yml to /plugins/Plex/modules/Plex-HTTPD/config.yml // Move it from /httpd/config.yml to /plugins/Plex/modules/Plex-HTTPD/config.yml
moduleConfig = new ModuleConfig(this, "httpd/config.yml", "config.yml"); moduleConfig = new ModuleConfig(this, "httpd/config.yml", "config.yml");
} }
@@ -70,17 +75,18 @@ public class HTTPDModule extends PlexModule
public void enable() public void enable()
{ {
moduleConfig.load(); moduleConfig.load();
HTTPDModule.plexApi().logging().debug("HTTPD Module Port: {0}", moduleConfig.getInt("server.port")); api().logging().debug("HTTPD Module Port: {0}", moduleConfig.getInt("server.port"));
accessLogFile = new File(getDataFolder(), moduleConfig.getString("server.logging.file-path", "httpd.log")); accessLogFile = new File(getDataFolder(), moduleConfig.getString("server.logging.file-path", "httpd.log"));
Log.configure(moduleConfig, accessLogFile);
minecraftAssetsManager = new MinecraftAssetsManager(getDataFolder().toPath()); minecraftAssetsManager = new MinecraftAssetsManager(getDataFolder().toPath(), api());
minecraftAssetsManager.refreshAsync(); minecraftAssetsManager.refreshAsync();
authenticationManager = new AuthenticationManager(); authenticationManager = new AuthenticationManager(this);
if (authenticationManager.provider() == null) if (authenticationManager.provider() == null)
{ {
HTTPDModule.plexApi().logging().debug("Authentication is disabled or misconfigured"); api().logging().debug("Authentication is disabled or misconfigured");
} }
@@ -110,33 +116,37 @@ public class HTTPDModule extends PlexModule
connector.setIdleTimeout(moduleConfig.getLong("server.limits.idle-timeout-ms", 15_000L)); connector.setIdleTimeout(moduleConfig.getLong("server.limits.idle-timeout-ms", 15_000L));
connector.setAcceptQueueSize(moduleConfig.getInt("server.limits.accept-queue", 32)); connector.setAcceptQueueSize(moduleConfig.getInt("server.limits.accept-queue", 32));
context.addFilter(new FilterHolder(new RateLimitFilter()), "/*", EnumSet.of(DispatcherType.REQUEST)); context.addFilter(new FilterHolder(new RateLimitFilter(moduleConfig)), "/*", EnumSet.of(DispatcherType.REQUEST));
StatsBroadcaster.get().start(); statsBroadcaster = new StatsBroadcaster(this);
PlayersBroadcaster.get().start(); playersBroadcaster = new PlayersBroadcaster(this);
PlayerInventoryBroadcaster.get().start(); playerInventoryBroadcaster = new PlayerInventoryBroadcaster(this);
statsBroadcaster.start();
playersBroadcaster.start();
playerInventoryBroadcaster.start();
new IndefBansEndpoint(); new IndefBansEndpoint(this);
new IndexEndpoint(); new IndexEndpoint(this);
new ListEndpoint(); new ListEndpoint(this);
new PunishmentsEndpoint(); new PunishmentsEndpoint(this);
new CommandsEndpoint(); new CommandsEndpoint(this);
new SchematicDownloadEndpoint(); new SchematicDownloadEndpoint(this);
new SchematicUploadEndpoint(); new SchematicUploadEndpoint(this);
new PlayersEndpoint(); new PlayersEndpoint(this);
new PlayerAdminEndpoint(); new PlayerAdminEndpoint(this);
new AssetsEndpoint(); new AssetsEndpoint(this);
new PunishmentsUIEndpoint(); new PunishmentsUIEndpoint(this);
new IndefBansUIEndpoint(); new IndefBansUIEndpoint(this);
new AuthenticationEndpoint(); new AuthenticationEndpoint(this);
HTTPDModule.context.addServlet(StatsStreamServlet.class, "/api/stats/stream"); context.addServlet(new ServletHolder(new StatsStreamServlet(statsBroadcaster)), "/api/stats/stream");
HTTPDModule.context.addServlet(PlayersStreamServlet.class, "/api/players/stream"); context.addServlet(new ServletHolder(new PlayersStreamServlet(playersBroadcaster)), "/api/players/stream");
HTTPDModule.context.addServlet(StaffPlayersStreamServlet.class, "/api/players/stream/staff"); context.addServlet(new ServletHolder(new StaffPlayersStreamServlet(this, playersBroadcaster)), "/api/players/stream/staff");
HTTPDModule.context.addServlet(PlayerActionServlet.class, "/api/admin/action"); context.addServlet(new ServletHolder(new PlayerActionServlet(this)), "/api/admin/action");
HTTPDModule.context.addServlet(PlayerInventoryStreamServlet.class, "/api/player/inventory/stream"); context.addServlet(new ServletHolder(new PlayerInventoryStreamServlet(this, playerInventoryBroadcaster)), "/api/player/inventory/stream");
ServletHolder uploadHolder = HTTPDModule.context.addServlet(SchematicUploadServlet.class, "/api/schematics/uploading"); ServletHolder uploadHolder = new ServletHolder(new SchematicUploadServlet(this));
context.addServlet(uploadHolder, "/api/schematics/uploading");
File uploadLoc = new File(System.getProperty("java.io.tmpdir"), "schematic-temp-dir"); File uploadLoc = new File(System.getProperty("java.io.tmpdir"), "schematic-temp-dir");
if (!uploadLoc.exists()) if (!uploadLoc.exists())
@@ -160,16 +170,19 @@ public class HTTPDModule extends PlexModule
} }
}, "Jetty-Server"); }, "Jetty-Server");
serverThread.start(); serverThread.start();
HTTPDModule.plexApi().logging().info("Starting Jetty server on port " + moduleConfig.getInt("server.port")); api().logging().info("Starting Jetty server on port " + moduleConfig.getInt("server.port"));
} }
@Override @Override
public void disable() public void disable()
{ {
HTTPDModule.plexApi().logging().debug("Stopping Jetty server"); api().logging().debug("Stopping Jetty server");
try try
{ {
StatsBroadcaster.get().shutdown(); if (statsBroadcaster != null)
{
statsBroadcaster.shutdown();
}
} }
catch (Throwable t) catch (Throwable t)
{ {
@@ -177,7 +190,10 @@ public class HTTPDModule extends PlexModule
} }
try try
{ {
PlayersBroadcaster.get().shutdown(); if (playersBroadcaster != null)
{
playersBroadcaster.shutdown();
}
} }
catch (Throwable t) catch (Throwable t)
{ {
@@ -185,7 +201,10 @@ public class HTTPDModule extends PlexModule
} }
try try
{ {
PlayerInventoryBroadcaster.get().shutdown(); if (playerInventoryBroadcaster != null)
{
playerInventoryBroadcaster.shutdown();
}
} }
catch (Throwable t) catch (Throwable t)
{ {
@@ -193,8 +212,12 @@ public class HTTPDModule extends PlexModule
} }
try try
{ {
atomicServer.get().stop(); Server server = atomicServer.get();
atomicServer.get().destroy(); if (server != null)
{
server.stop();
server.destroy();
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -1,6 +1,6 @@
package dev.plex.assets; package dev.plex.assets;
import dev.plex.HTTPDModule; import dev.plex.api.PlexApi;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -35,12 +35,14 @@ public class MinecraftAssetsManager
private final AtomicBoolean ready = new AtomicBoolean(false); private final AtomicBoolean ready = new AtomicBoolean(false);
private final AtomicBoolean refreshStarted = new AtomicBoolean(false); private final AtomicBoolean refreshStarted = new AtomicBoolean(false);
private final String minecraftVersion; private final String minecraftVersion;
private final PlexApi api;
public MinecraftAssetsManager(Path dataFolder) public MinecraftAssetsManager(Path dataFolder, PlexApi api)
{ {
this.root = dataFolder.resolve("minecraft-assets"); this.root = dataFolder.resolve("minecraft-assets");
this.versionFile = root.resolve("version.txt"); this.versionFile = root.resolve("version.txt");
this.minecraftVersion = detectMinecraftVersion(); this.minecraftVersion = detectMinecraftVersion();
this.api = api;
this.client = HttpClient.newBuilder() this.client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL) .followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20)) .connectTimeout(Duration.ofSeconds(20))
@@ -63,7 +65,7 @@ public class MinecraftAssetsManager
} }
catch (Exception e) catch (Exception e)
{ {
HTTPDModule.plexApi().logging().info("Unable to download Minecraft assets for HTTPD inventory view: " + e.getMessage()); api.logging().info("Unable to download Minecraft assets for HTTPD inventory view: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
} }
}); });
@@ -90,13 +92,13 @@ public class MinecraftAssetsManager
String cachedVersion = Files.exists(versionFile) ? Files.readString(versionFile).trim() : ""; String cachedVersion = Files.exists(versionFile) ? Files.readString(versionFile).trim() : "";
if (minecraftVersion.equals(cachedVersion) && hasAssets()) if (minecraftVersion.equals(cachedVersion) && hasAssets())
{ {
HTTPDModule.plexApi().logging().debug("HTTPD Minecraft assets are already cached for {0}", minecraftVersion); api.logging().debug("HTTPD Minecraft assets are already cached for {0}", minecraftVersion);
return; return;
} }
if (!cachedVersion.isEmpty() && !minecraftVersion.equals(cachedVersion)) if (!cachedVersion.isEmpty() && !minecraftVersion.equals(cachedVersion))
{ {
HTTPDModule.plexApi().logging().info("Minecraft version changed from " + cachedVersion + " to " + minecraftVersion + "; recreating HTTPD asset cache"); api.logging().info("Minecraft version changed from " + cachedVersion + " to " + minecraftVersion + "; recreating HTTPD asset cache");
} }
recreateCache(); recreateCache();
} }
@@ -114,7 +116,7 @@ public class MinecraftAssetsManager
deleteDirectory(root); deleteDirectory(root);
Files.createDirectories(root); Files.createDirectories(root);
HTTPDModule.plexApi().logging().info("Downloading Minecraft " + minecraftVersion + " client assets for HTTPD inventory view"); api.logging().info("Downloading Minecraft " + minecraftVersion + " client assets for HTTPD inventory view");
JSONObject version = findVersionJson(); JSONObject version = findVersionJson();
String clientUrl = version.getJSONObject("downloads").getJSONObject("client").getString("url"); String clientUrl = version.getJSONObject("downloads").getJSONObject("client").getString("url");
@@ -142,7 +144,7 @@ public class MinecraftAssetsManager
} }
Files.writeString(versionFile, minecraftVersion + System.lineSeparator()); Files.writeString(versionFile, minecraftVersion + System.lineSeparator());
HTTPDModule.plexApi().logging().info("HTTPD Minecraft assets cached for " + minecraftVersion); api.logging().info("HTTPD Minecraft assets cached for " + minecraftVersion);
} }
private JSONObject findVersionJson() throws IOException, InterruptedException private JSONObject findVersionJson() throws IOException, InterruptedException
@@ -7,17 +7,17 @@ public class AuthenticationManager
{ {
private final OAuth2Provider provider; private final OAuth2Provider provider;
public AuthenticationManager() public AuthenticationManager(HTTPDModule module)
{ {
final boolean enabled = HTTPDModule.moduleConfig.getBoolean("authentication.enabled", false); final boolean enabled = module.getModuleConfig().getBoolean("authentication.enabled", false);
if (!enabled) if (!enabled)
{ {
provider = null; provider = null;
return; return;
} }
HTTPDModule.plexApi().logging().info("[HTTPD] XenForo OAuth2 authentication is enabled"); module.api().logging().info("[HTTPD] XenForo OAuth2 authentication is enabled");
provider = new XenForoOAuth2Provider(); provider = new XenForoOAuth2Provider(module);
} }
public OAuth2Provider provider() public OAuth2Provider provider()
@@ -49,19 +49,21 @@ public class XenForoOAuth2Provider implements OAuth2Provider
private final String clientSecret; private final String clientSecret;
private final String redirectUri; private final String redirectUri;
private final Duration sessionTtl; private final Duration sessionTtl;
private final HTTPDModule module;
public XenForoOAuth2Provider() public XenForoOAuth2Provider(HTTPDModule module)
{ {
String domain = HTTPDModule.moduleConfig.getString("authentication.provider.xenforo.domain", ""); this.module = module;
this.clientId = HTTPDModule.moduleConfig.getString("authentication.provider.xenforo.clientId", ""); String domain = module.getModuleConfig().getString("authentication.provider.xenforo.domain", "");
this.clientSecret = HTTPDModule.moduleConfig.getString("authentication.provider.xenforo.clientSecret", ""); this.clientId = module.getModuleConfig().getString("authentication.provider.xenforo.clientId", "");
this.redirectUri = HTTPDModule.moduleConfig.getString("authentication.provider.redirectUri", ""); this.clientSecret = module.getModuleConfig().getString("authentication.provider.xenforo.clientSecret", "");
long ttlMinutes = HTTPDModule.moduleConfig.getLong("authentication.provider.xenforo.sessionMinutes", 1440L); this.redirectUri = module.getModuleConfig().getString("authentication.provider.redirectUri", "");
long ttlMinutes = module.getModuleConfig().getLong("authentication.provider.xenforo.sessionMinutes", 1440L);
this.sessionTtl = Duration.ofMinutes(Math.max(ttlMinutes, 1L)); this.sessionTtl = Duration.ofMinutes(Math.max(ttlMinutes, 1L));
if (domain.isEmpty() || clientId.isEmpty() || clientSecret.isEmpty() || redirectUri.isEmpty()) if (domain.isEmpty() || clientId.isEmpty() || clientSecret.isEmpty() || redirectUri.isEmpty())
{ {
HTTPDModule.plexApi().logging().error("XenForo OAuth2 misconfigured: domain, clientId, clientSecret, redirectUri are all required."); module.api().logging().error("XenForo OAuth2 misconfigured: domain, clientId, clientSecret, redirectUri are all required.");
} }
String base = "https://" + domain.replaceFirst("^https?://", "").replaceAll("/+$", ""); String base = "https://" + domain.replaceFirst("^https?://", "").replaceAll("/+$", "");
@@ -285,7 +287,7 @@ public class XenForoOAuth2Provider implements OAuth2Provider
} }
catch (Exception e) catch (Exception e)
{ {
HTTPDModule.plexApi().logging().debug("Failed to revoke XenForo token: {0}", e.getMessage()); module.api().logging().debug("Failed to revoke XenForo token: {0}", e.getMessage());
} }
} }
+18 -5
View File
@@ -1,6 +1,6 @@
package dev.plex.logging; package dev.plex.logging;
import dev.plex.HTTPDModule; import dev.plex.config.ModuleConfig;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -11,19 +11,30 @@ import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.function.BooleanSupplier;
public class Log public class Log
{ {
private static final DateTimeFormatter STAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS z"); private static final DateTimeFormatter STAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS z");
private static BooleanSupplier consoleLoggingEnabled = () -> false;
private static BooleanSupplier fileLoggingEnabled = () -> false;
private static File accessLogFile;
private static BufferedWriter writer; private static BufferedWriter writer;
private static File writerTarget; private static File writerTarget;
public static synchronized void configure(ModuleConfig moduleConfig, File target)
{
consoleLoggingEnabled = () -> moduleConfig.getBoolean("server.logging.console", false);
fileLoggingEnabled = () -> moduleConfig.getBoolean("server.logging.file", true);
accessLogFile = target;
}
public static void log(String message, Object... strings) public static void log(String message, Object... strings)
{ {
String formatted = format(message, strings); String formatted = format(message, strings);
writeFile(formatted); writeFile(formatted);
if (HTTPDModule.moduleConfig != null && HTTPDModule.moduleConfig.getBoolean("server.logging.console", false)) if (consoleLoggingEnabled.getAsBoolean())
{ {
Bukkit.getConsoleSender().sendMessage(Component.text("[Plex HTTPD] ").color(NamedTextColor.DARK_AQUA).append(Component.text(formatted).color(NamedTextColor.GRAY))); Bukkit.getConsoleSender().sendMessage(Component.text("[Plex HTTPD] ").color(NamedTextColor.DARK_AQUA).append(Component.text(formatted).color(NamedTextColor.GRAY)));
} }
@@ -42,6 +53,9 @@ public class Log
writer = null; writer = null;
writerTarget = null; writerTarget = null;
} }
consoleLoggingEnabled = () -> false;
fileLoggingEnabled = () -> false;
accessLogFile = null;
} }
private static String format(String message, Object... strings) private static String format(String message, Object... strings)
@@ -59,9 +73,8 @@ public class Log
private static synchronized void writeFile(String formatted) private static synchronized void writeFile(String formatted)
{ {
if (HTTPDModule.moduleConfig == null) return; if (!fileLoggingEnabled.getAsBoolean()) return;
if (!HTTPDModule.moduleConfig.getBoolean("server.logging.file", true)) return; File target = accessLogFile;
File target = HTTPDModule.getAccessLogFile();
if (target == null) return; if (target == null) return;
if (writer == null || !target.equals(writerTarget)) if (writer == null || !target.equals(writerTarget))
{ {
@@ -1,6 +1,6 @@
package dev.plex.ratelimit; package dev.plex.ratelimit;
import dev.plex.HTTPDModule; import dev.plex.config.ModuleConfig;
import dev.plex.logging.Log; import dev.plex.logging.Log;
import jakarta.servlet.Filter; import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
@@ -28,14 +28,14 @@ public class RateLimitFilter implements Filter
private final ConcurrentHashMap<String, TokenBucket> ipBuckets = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, TokenBucket> ipBuckets = new ConcurrentHashMap<>();
private final AtomicLong nextEvictMillis = new AtomicLong(System.currentTimeMillis() + EVICT_INTERVAL_MILLIS); private final AtomicLong nextEvictMillis = new AtomicLong(System.currentTimeMillis() + EVICT_INTERVAL_MILLIS);
public RateLimitFilter() public RateLimitFilter(ModuleConfig config)
{ {
this.enabled = HTTPDModule.moduleConfig.getBoolean("rate-limit.enabled", true); this.enabled = config.getBoolean("rate-limit.enabled", true);
double globalCapacity = HTTPDModule.moduleConfig.getDouble("rate-limit.global.capacity", 200.0); double globalCapacity = config.getDouble("rate-limit.global.capacity", 200.0);
double globalRate = HTTPDModule.moduleConfig.getDouble("rate-limit.global.per-second", 100.0); double globalRate = config.getDouble("rate-limit.global.per-second", 100.0);
this.globalBucket = new TokenBucket(globalCapacity, globalRate); this.globalBucket = new TokenBucket(globalCapacity, globalRate);
this.ipCapacity = HTTPDModule.moduleConfig.getDouble("rate-limit.per-ip.capacity", 30.0); this.ipCapacity = config.getDouble("rate-limit.per-ip.capacity", 30.0);
this.ipRefillPerSecond = HTTPDModule.moduleConfig.getDouble("rate-limit.per-ip.per-second", 10.0); this.ipRefillPerSecond = config.getDouble("rate-limit.per-ip.per-second", 10.0);
} }
@Override @Override
@@ -28,9 +28,11 @@ import org.eclipse.jetty.ee10.servlet.ServletHolder;
public class AbstractServlet extends HttpServlet public class AbstractServlet extends HttpServlet
{ {
private final List<Mapping> GET_MAPPINGS = Lists.newArrayList(); private final List<Mapping> GET_MAPPINGS = Lists.newArrayList();
protected final HTTPDModule module;
public AbstractServlet() public AbstractServlet(HTTPDModule module)
{ {
this.module = module;
for (Method declaredMethod : this.getClass().getDeclaredMethods()) for (Method declaredMethod : this.getClass().getDeclaredMethods())
{ {
declaredMethod.setAccessible(true); declaredMethod.setAccessible(true);
@@ -46,7 +48,7 @@ public class AbstractServlet extends HttpServlet
ServletHolder holder = new ServletHolder(this); ServletHolder holder = new ServletHolder(this);
String endpoint = getMapping.endpoint(); String endpoint = getMapping.endpoint();
String pattern = endpoint.endsWith("/") ? endpoint + "*" : endpoint; String pattern = endpoint.endsWith("/") ? endpoint + "*" : endpoint;
HTTPDModule.context.addServlet(holder, pattern); module.getContext().addServlet(holder, pattern);
} }
} }
} }
@@ -63,11 +65,6 @@ public class AbstractServlet extends HttpServlet
String requestPath = getRequestPath(req); String requestPath = getRequestPath(req);
Log.log(ipAddress + " visited endpoint " + requestPath); Log.log(ipAddress + " visited endpoint " + requestPath);
/*Enumeration<String> headerz = req.getHeaderNames();
while (headerz.hasMoreElements()) {
String header = headerz.nextElement();
HTTPDModule.plexApi().logging().debug("Header: {0} Value {1}", header, req.getHeader(header));
}*/
GET_MAPPINGS.stream().filter(mapping -> endpointMatchesRequest(mapping.getMapping().endpoint(), requestPath)).forEach(mapping -> GET_MAPPINGS.stream().filter(mapping -> endpointMatchesRequest(mapping.getMapping().endpoint(), requestPath)).forEach(mapping ->
{ {
resp.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8");
@@ -137,27 +134,42 @@ public class AbstractServlet extends HttpServlet
return requestPath.isEmpty() ? "/" : requestPath; return requestPath.isEmpty() ? "/" : requestPath;
} }
public static AuthenticatedUser currentUser(HttpServletRequest request) protected AuthenticatedUser currentUser(HttpServletRequest request)
{ {
AuthenticationManager manager = HTTPDModule.getAuthenticationManager(); return currentUser(module, request);
}
public static AuthenticatedUser currentUser(HTTPDModule module, HttpServletRequest request)
{
AuthenticationManager manager = module.getAuthenticationManager();
if (manager == null) return null; if (manager == null) return null;
OAuth2Provider provider = manager.provider(); OAuth2Provider provider = manager.provider();
if (provider == null) return null; if (provider == null) return null;
return provider.lookup(request); return provider.lookup(request);
} }
public static AuthenticatedUser currentStaff(HttpServletRequest request) protected AuthenticatedUser currentStaff(HttpServletRequest request)
{ {
AuthenticatedUser user = currentUser(request); return currentStaff(module, request);
}
public static AuthenticatedUser currentStaff(HTTPDModule module, HttpServletRequest request)
{
AuthenticatedUser user = currentUser(module, request);
return (user != null && user.staff()) ? user : null; return (user != null && user.staff()) ? user : null;
} }
public static String signInPrompt(String action) protected String signInPrompt(String action)
{ {
return signInPrompt(null, action); return signInPrompt(null, action);
} }
public static String signInPrompt(HttpServletRequest request, String action) protected String signInPrompt(HttpServletRequest request, String action)
{
return signInPrompt(module, request, action);
}
public static String signInPrompt(HTTPDModule module, HttpServletRequest request, String action)
{ {
String href = "/oauth2/login"; String href = "/oauth2/login";
if (request != null) if (request != null)
@@ -170,9 +182,14 @@ public class AbstractServlet extends HttpServlet
return "You must <a class=\"text-primary underline\" href=\"" + href + "\">sign in</a> as staff " + action + "."; return "You must <a class=\"text-primary underline\" href=\"" + href + "\">sign in</a> as staff " + action + ".";
} }
public static String readFile(InputStream filename) protected String readFile(InputStream filename)
{ {
String base = HTTPDModule.template; return readFile(module, filename);
}
public static String readFile(HTTPDModule module, InputStream filename)
{
String base = module.getTemplate();
String page = readFileReal(filename); String page = readFileReal(filename);
String[] info = page.split("\\r?\\n", 3); String[] info = page.split("\\r?\\n", 3);
String title = info.length > 0 ? info[0] : ""; String title = info.length > 0 ? info[0] : "";
@@ -27,12 +27,18 @@ public class PlayerActionServlet extends HttpServlet
private static final List<String> PERMANENT_ACTIONS = List.of("ban", "mute"); private static final List<String> PERMANENT_ACTIONS = List.of("ban", "mute");
private static final List<String> TEMP_ACTIONS = List.of("tempban", "tempmute", "freeze"); private static final List<String> TEMP_ACTIONS = List.of("tempban", "tempmute", "freeze");
private static final List<String> INVENTORY_ACTIONS = List.of("clear-inventory", "clear-selected"); private static final List<String> INVENTORY_ACTIONS = List.of("clear-inventory", "clear-selected");
private final HTTPDModule module;
public PlayerActionServlet(HTTPDModule module)
{
this.module = module;
}
@Override @Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
{ {
AuthenticatedUser staff = AbstractServlet.currentStaff(request); AuthenticatedUser staff = AbstractServlet.currentStaff(module, request);
if (staff == null) if (staff == null)
{ {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
@@ -71,7 +77,7 @@ public class PlayerActionServlet extends HttpServlet
return; return;
} }
PlexPlayerView target = HTTPDModule.plexApi().players().byUuid(uuid).orElse(null); PlexPlayerView target = module.api().players().byUuid(uuid).orElse(null);
if (target == null) if (target == null)
{ {
response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.setStatus(HttpServletResponse.SC_NOT_FOUND);
@@ -119,11 +125,11 @@ public class PlayerActionServlet extends HttpServlet
final boolean kick = action.equals("ban") || action.equals("tempban"); final boolean kick = action.equals("ban") || action.equals("tempban");
final PunishmentRequest toApply = punishment; final PunishmentRequest toApply = punishment;
HTTPDModule.plexApi().scheduler().runGlobal(() -> module.api().scheduler().runGlobal(() ->
{ {
try try
{ {
HTTPDModule.plexApi().punishments().punish(target, toApply); module.api().punishments().punish(target, toApply);
} }
catch (Throwable t) catch (Throwable t)
{ {
@@ -135,7 +141,7 @@ public class PlayerActionServlet extends HttpServlet
Player online = Bukkit.getPlayer(uuid); Player online = Bukkit.getPlayer(uuid);
if (online != null) if (online != null)
{ {
HTTPDModule.plexApi().scheduler().runEntity(online, () -> module.api().scheduler().runEntity(online, () ->
{ {
try { online.kick(Component.text("You have been banned: " + toApply.reason())); } try { online.kick(Component.text("You have been banned: " + toApply.reason())); }
catch (Throwable t) { t.printStackTrace(); } catch (Throwable t) { t.printStackTrace(); }
@@ -147,7 +153,7 @@ public class PlayerActionServlet extends HttpServlet
response.sendRedirect("/player/" + uuid); response.sendRedirect("/player/" + uuid);
} }
private static void handleInventoryAction(HttpServletRequest request, HttpServletResponse response, AuthenticatedUser staff, UUID uuid, PlexPlayerView target, String action, String slot) private void handleInventoryAction(HttpServletRequest request, HttpServletResponse response, AuthenticatedUser staff, UUID uuid, PlexPlayerView target, String action, String slot)
throws IOException throws IOException
{ {
String ipAddress = request.getRemoteAddr(); String ipAddress = request.getRemoteAddr();
@@ -159,11 +165,11 @@ public class PlayerActionServlet extends HttpServlet
Log.log(ipAddress + " (xf:" + staff.username() + ") issued " + action + " on " + target.name() + " (" + uuid + ")" + (slot == null || slot.isBlank() ? "" : " slot " + slot)); Log.log(ipAddress + " (xf:" + staff.username() + ") issued " + action + " on " + target.name() + " (" + uuid + ")" + (slot == null || slot.isBlank() ? "" : " slot " + slot));
HTTPDModule.plexApi().scheduler().runGlobal(() -> module.api().scheduler().runGlobal(() ->
{ {
Player online = Bukkit.getPlayer(uuid); Player online = Bukkit.getPlayer(uuid);
if (online == null) return; if (online == null) return;
HTTPDModule.plexApi().scheduler().runEntity(online, () -> module.api().scheduler().runEntity(online, () ->
{ {
PlayerInventory inv = online.getInventory(); PlayerInventory inv = online.getInventory();
if ("clear-inventory".equals(action)) if ("clear-inventory".equals(action))
@@ -1,5 +1,6 @@
package dev.plex.request; package dev.plex.request;
import dev.plex.HTTPDModule;
import dev.plex.logging.Log; import dev.plex.logging.Log;
import dev.plex.request.impl.PlayerInventoryBroadcaster; import dev.plex.request.impl.PlayerInventoryBroadcaster;
import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncContext;
@@ -16,11 +17,20 @@ import java.util.UUID;
public class PlayerInventoryStreamServlet extends HttpServlet public class PlayerInventoryStreamServlet extends HttpServlet
{ {
private final HTTPDModule module;
private final PlayerInventoryBroadcaster broadcaster;
public PlayerInventoryStreamServlet(HTTPDModule module, PlayerInventoryBroadcaster broadcaster)
{
this.module = module;
this.broadcaster = broadcaster;
}
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
{ {
if (AbstractServlet.currentStaff(request) == null) if (AbstractServlet.currentStaff(module, request) == null)
{ {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return; return;
@@ -51,7 +61,6 @@ public class PlayerInventoryStreamServlet extends HttpServlet
} }
Log.log(ipAddress + " opened inventory stream for " + uuid); Log.log(ipAddress + " opened inventory stream for " + uuid);
PlayerInventoryBroadcaster broadcaster = PlayerInventoryBroadcaster.get();
if (broadcaster.atCapacity()) if (broadcaster.atCapacity())
{ {
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
@@ -15,6 +15,13 @@ import java.io.PrintWriter;
public class PlayersStreamServlet extends HttpServlet public class PlayersStreamServlet extends HttpServlet
{ {
private final PlayersBroadcaster broadcaster;
public PlayersStreamServlet(PlayersBroadcaster broadcaster)
{
this.broadcaster = broadcaster;
}
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
@@ -27,7 +34,6 @@ public class PlayersStreamServlet extends HttpServlet
} }
Log.log(ipAddress + " opened SSE stream /api/players/stream"); Log.log(ipAddress + " opened SSE stream /api/players/stream");
PlayersBroadcaster broadcaster = PlayersBroadcaster.get();
if (broadcaster.atCapacity()) if (broadcaster.atCapacity())
{ {
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
@@ -22,14 +22,20 @@ import java.util.regex.Pattern;
public class SchematicUploadServlet extends HttpServlet public class SchematicUploadServlet extends HttpServlet
{ {
private static final Pattern schemNameMatcher = Pattern.compile("^[a-z0-9'!,_ -]{1,30}\\.schem(atic)?$", Pattern.CASE_INSENSITIVE); private static final Pattern schemNameMatcher = Pattern.compile("^[a-z0-9'!,_ -]{1,30}\\.schem(atic)?$", Pattern.CASE_INSENSITIVE);
private final HTTPDModule module;
public SchematicUploadServlet(HTTPDModule module)
{
this.module = module;
}
@Override @Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{ {
AuthenticatedUser user = AbstractServlet.currentStaff(request); AuthenticatedUser user = AbstractServlet.currentStaff(module, request);
if (user == null) if (user == null)
{ {
response.getWriter().println(schematicUploadBadHTML(AbstractServlet.signInPrompt(request, "to upload schematics"))); response.getWriter().println(schematicUploadBadHTML(AbstractServlet.signInPrompt(module, request, "to upload schematics")));
return; return;
} }
File worldeditFolder = HTTPDModule.getWorldeditFolder(); File worldeditFolder = HTTPDModule.getWorldeditFolder();
@@ -67,7 +73,7 @@ public class SchematicUploadServlet extends HttpServlet
ClipboardFormat schematicFormat = ClipboardFormats.findByFile(schematicFile); ClipboardFormat schematicFormat = ClipboardFormats.findByFile(schematicFile);
if (schematicFormat == null) if (schematicFormat == null)
{ {
HTTPDModule.plexApi().logging().info(user.username() + " FAILED to upload schematic with filename: " + filename); module.api().logging().info(user.username() + " FAILED to upload schematic with filename: " + filename);
Log.log("{0} (xf:{1}) FAILED to upload schematic {2}", user.username(), user.userId(), filename); Log.log("{0} (xf:{1}) FAILED to upload schematic {2}", user.username(), user.userId(), filename);
response.getWriter().println(schematicUploadBadHTML("Schematic is not a valid format.")); response.getWriter().println(schematicUploadBadHTML("Schematic is not a valid format."));
FileUtils.deleteQuietly(schematicFile); FileUtils.deleteQuietly(schematicFile);
@@ -79,7 +85,7 @@ public class SchematicUploadServlet extends HttpServlet
} }
catch (IOException e) catch (IOException e)
{ {
HTTPDModule.plexApi().logging().info(user.username() + " FAILED to upload schematic with filename: " + filename); module.api().logging().info(user.username() + " FAILED to upload schematic with filename: " + filename);
Log.log("{0} (xf:{1}) FAILED to upload schematic {2}", user.username(), user.userId(), filename); Log.log("{0} (xf:{1}) FAILED to upload schematic {2}", user.username(), user.userId(), filename);
response.getWriter().println(schematicUploadBadHTML("Schematic is not a valid format.")); response.getWriter().println(schematicUploadBadHTML("Schematic is not a valid format."));
FileUtils.deleteQuietly(schematicFile); FileUtils.deleteQuietly(schematicFile);
@@ -87,20 +93,20 @@ public class SchematicUploadServlet extends HttpServlet
} }
inputStream.close(); inputStream.close();
response.getWriter().println(schematicUploadGoodHTML("Successfully uploaded <b>" + filename + "</b>.")); response.getWriter().println(schematicUploadGoodHTML("Successfully uploaded <b>" + filename + "</b>."));
HTTPDModule.plexApi().logging().info(user.username() + " uploaded schematic with filename: " + filename); module.api().logging().info(user.username() + " uploaded schematic with filename: " + filename);
Log.log("{0} (xf:{1}) uploaded schematic {2}", user.username(), user.userId(), filename); Log.log("{0} (xf:{1}) uploaded schematic {2}", user.username(), user.userId(), filename);
} }
private String schematicUploadBadHTML(String message) private String schematicUploadBadHTML(String message)
{ {
String file = AbstractServlet.readFile(this.getClass().getResourceAsStream("/httpd/schematic_upload_bad.html")); String file = AbstractServlet.readFile(module, this.getClass().getResourceAsStream("/httpd/schematic_upload_bad.html"));
file = file.replace("${MESSAGE}", message); file = file.replace("${MESSAGE}", message);
return file; return file;
} }
private String schematicUploadGoodHTML(String message) private String schematicUploadGoodHTML(String message)
{ {
String file = AbstractServlet.readFile(this.getClass().getResourceAsStream("/httpd/schematic_upload_good.html")); String file = AbstractServlet.readFile(module, this.getClass().getResourceAsStream("/httpd/schematic_upload_good.html"));
file = file.replace("${MESSAGE}", message); file = file.replace("${MESSAGE}", message);
return file; return file;
} }
@@ -1,5 +1,6 @@
package dev.plex.request; package dev.plex.request;
import dev.plex.HTTPDModule;
import dev.plex.logging.Log; import dev.plex.logging.Log;
import dev.plex.request.impl.PlayersBroadcaster; import dev.plex.request.impl.PlayersBroadcaster;
import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncContext;
@@ -15,11 +16,20 @@ import java.io.PrintWriter;
public class StaffPlayersStreamServlet extends HttpServlet public class StaffPlayersStreamServlet extends HttpServlet
{ {
private final HTTPDModule module;
private final PlayersBroadcaster broadcaster;
public StaffPlayersStreamServlet(HTTPDModule module, PlayersBroadcaster broadcaster)
{
this.module = module;
this.broadcaster = broadcaster;
}
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
{ {
if (AbstractServlet.currentStaff(request) == null) if (AbstractServlet.currentStaff(module, request) == null)
{ {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return; return;
@@ -33,7 +43,6 @@ public class StaffPlayersStreamServlet extends HttpServlet
} }
Log.log(ipAddress + " opened SSE stream /api/players/stream/staff"); Log.log(ipAddress + " opened SSE stream /api/players/stream/staff");
PlayersBroadcaster broadcaster = PlayersBroadcaster.get();
if (broadcaster.atCapacity()) if (broadcaster.atCapacity())
{ {
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
@@ -15,6 +15,13 @@ import java.io.PrintWriter;
public class StatsStreamServlet extends HttpServlet public class StatsStreamServlet extends HttpServlet
{ {
private final StatsBroadcaster broadcaster;
public StatsStreamServlet(StatsBroadcaster broadcaster)
{
this.broadcaster = broadcaster;
}
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
@@ -27,7 +34,6 @@ public class StatsStreamServlet extends HttpServlet
} }
Log.log(ipAddress + " opened SSE stream /api/stats/stream"); Log.log(ipAddress + " opened SSE stream /api/stats/stream");
StatsBroadcaster broadcaster = StatsBroadcaster.get();
if (broadcaster.atCapacity()) if (broadcaster.atCapacity())
{ {
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
@@ -20,6 +20,10 @@ public class AssetsEndpoint extends AbstractServlet
private static final Pattern MODEL_PATH = Pattern.compile("(item|block)/[a-z0-9_]+\\.json"); private static final Pattern MODEL_PATH = Pattern.compile("(item|block)/[a-z0-9_]+\\.json");
private static final Pattern ITEM_DEF_PATH = Pattern.compile("[a-z0-9_]+\\.json"); private static final Pattern ITEM_DEF_PATH = Pattern.compile("[a-z0-9_]+\\.json");
public AssetsEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/assets/dashboard.js") @GetMapping(endpoint = "/assets/dashboard.js")
@MappingHeaders(headers = {"content-type;application/javascript; charset=utf-8", "cache-control;public, max-age=300"}) @MappingHeaders(headers = {"content-type;application/javascript; charset=utf-8", "cache-control;public, max-age=300"})
@@ -97,7 +101,7 @@ public class AssetsEndpoint extends AbstractServlet
return null; return null;
} }
private static void servePathUnder(HttpServletRequest request, HttpServletResponse response, String urlPrefix, Pattern allowed, String cacheCategory, String resourcePrefix) private void servePathUnder(HttpServletRequest request, HttpServletResponse response, String urlPrefix, Pattern allowed, String cacheCategory, String resourcePrefix)
{ {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
if (!uri.startsWith(urlPrefix)) if (!uri.startsWith(urlPrefix))
@@ -123,13 +127,13 @@ public class AssetsEndpoint extends AbstractServlet
serveResource(resourcePrefix + rest, response); serveResource(resourcePrefix + rest, response);
} }
private static boolean serveCached(String category, String relativePath, HttpServletResponse response) private boolean serveCached(String category, String relativePath, HttpServletResponse response)
{ {
if (HTTPDModule.getMinecraftAssetsManager() == null) if (module.getMinecraftAssetsManager() == null)
{ {
return false; return false;
} }
Path path = HTTPDModule.getMinecraftAssetsManager().resolve(category, relativePath); Path path = module.getMinecraftAssetsManager().resolve(category, relativePath);
if (path == null) if (path == null)
{ {
return false; return false;
@@ -21,10 +21,15 @@ public class AuthenticationEndpoint extends AbstractServlet
{ {
private static final String RETURN_TO_COOKIE = "plex_return_to"; private static final String RETURN_TO_COOKIE = "plex_return_to";
public AuthenticationEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/oauth2/login") @GetMapping(endpoint = "/oauth2/login")
public String login(HttpServletRequest request, HttpServletResponse response) throws IOException public String login(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
OAuth2Provider provider = HTTPDModule.getAuthenticationManager().provider(); OAuth2Provider provider = module.getAuthenticationManager().provider();
if (provider == null) if (provider == null)
{ {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Authentication is not enabled."); response.sendError(HttpServletResponse.SC_NOT_FOUND, "Authentication is not enabled.");
@@ -51,7 +56,7 @@ public class AuthenticationEndpoint extends AbstractServlet
@GetMapping(endpoint = "/oauth2/callback") @GetMapping(endpoint = "/oauth2/callback")
public String callback(HttpServletRequest request, HttpServletResponse response) throws IOException public String callback(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
OAuth2Provider provider = HTTPDModule.getAuthenticationManager().provider(); OAuth2Provider provider = module.getAuthenticationManager().provider();
if (provider == null) if (provider == null)
{ {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Authentication is not enabled."); response.sendError(HttpServletResponse.SC_NOT_FOUND, "Authentication is not enabled.");
@@ -63,7 +68,7 @@ public class AuthenticationEndpoint extends AbstractServlet
} }
catch (AuthenticationException e) catch (AuthenticationException e)
{ {
HTTPDModule.plexApi().logging().error("OAuth2 callback failed: " + e.getMessage()); module.api().logging().error("OAuth2 callback failed: " + e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("text/html; charset=UTF-8"); response.setContentType("text/html; charset=UTF-8");
return "<!doctype html><meta charset=utf-8><title>Sign-in failed</title>" return "<!doctype html><meta charset=utf-8><title>Sign-in failed</title>"
@@ -84,7 +89,7 @@ public class AuthenticationEndpoint extends AbstractServlet
@GetMapping(endpoint = "/oauth2/logout") @GetMapping(endpoint = "/oauth2/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) throws IOException public String logout(HttpServletRequest request, HttpServletResponse response) throws IOException
{ {
OAuth2Provider provider = HTTPDModule.getAuthenticationManager().provider(); OAuth2Provider provider = module.getAuthenticationManager().provider();
if (provider == null) if (provider == null)
{ {
response.sendRedirect("/"); response.sendRedirect("/");
@@ -106,7 +111,7 @@ public class AuthenticationEndpoint extends AbstractServlet
@MappingHeaders(headers = "content-type;application/json") @MappingHeaders(headers = "content-type;application/json")
public String me(HttpServletRequest request, HttpServletResponse response) public String me(HttpServletRequest request, HttpServletResponse response)
{ {
OAuth2Provider provider = HTTPDModule.getAuthenticationManager().provider(); OAuth2Provider provider = module.getAuthenticationManager().provider();
if (provider == null) if (provider == null)
{ {
return "{\"authenticated\":false,\"reason\":\"disabled\"}"; return "{\"authenticated\":false,\"reason\":\"disabled\"}";
@@ -22,6 +22,11 @@ public class CommandsEndpoint extends AbstractServlet
{ {
private String cachedHtml; private String cachedHtml;
public CommandsEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/api/commands/") @GetMapping(endpoint = "/api/commands/")
public String getCommands(HttpServletRequest request, HttpServletResponse response) public String getCommands(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -34,12 +39,12 @@ public class CommandsEndpoint extends AbstractServlet
return file; return file;
} }
private static String buildSections() private String buildSections()
{ {
final SortedMap<String, List<CommandInfo>> commandMap = new TreeMap<>(); final SortedMap<String, List<CommandInfo>> commandMap = new TreeMap<>();
List<CommandInfo> plexCommands = commandMap.computeIfAbsent("Plex", k -> new ArrayList<>()); List<CommandInfo> plexCommands = commandMap.computeIfAbsent("Plex", k -> new ArrayList<>());
for (PlexCommand command : HTTPDModule.plexApi().commands().registeredCommands()) for (PlexCommand command : module.api().commands().registeredCommands())
{ {
plexCommands.add(CommandInfo.from(command)); plexCommands.add(CommandInfo.from(command));
} }
@@ -10,6 +10,11 @@ import jakarta.servlet.http.HttpServletResponse;
public class IndefBansEndpoint extends AbstractServlet public class IndefBansEndpoint extends AbstractServlet
{ {
public IndefBansEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/api/indefbans/") @GetMapping(endpoint = "/api/indefbans/")
public String getBans(HttpServletRequest request, HttpServletResponse response) public String getBans(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -20,7 +25,7 @@ public class IndefBansEndpoint extends AbstractServlet
} }
response.setHeader("content-type", "application/json"); response.setHeader("content-type", "application/json");
return new GsonBuilder().setPrettyPrinting().create().toJson(HTTPDModule.plexApi().punishments().indefiniteBans()); return new GsonBuilder().setPrettyPrinting().create().toJson(module.api().punishments().indefiniteBans());
} }
private String indefbansHTML(String message) private String indefbansHTML(String message)
@@ -13,6 +13,11 @@ import java.util.UUID;
public class IndefBansUIEndpoint extends AbstractServlet public class IndefBansUIEndpoint extends AbstractServlet
{ {
public IndefBansUIEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/indefbans/") @GetMapping(endpoint = "/indefbans/")
public String getBans(HttpServletRequest request, HttpServletResponse response) public String getBans(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -22,7 +27,7 @@ public class IndefBansUIEndpoint extends AbstractServlet
return errorHTML(signInPrompt(request, "to view this page")); return errorHTML(signInPrompt(request, "to view this page"));
} }
List<? extends IndefiniteBanView> bans = HTTPDModule.plexApi().punishments().indefiniteBans(); List<? extends IndefiniteBanView> bans = module.api().punishments().indefiniteBans();
return listHTML(bans); return listHTML(bans);
} }
@@ -1,5 +1,6 @@
package dev.plex.request.impl; package dev.plex.request.impl;
import dev.plex.HTTPDModule;
import dev.plex.request.AbstractServlet; import dev.plex.request.AbstractServlet;
import dev.plex.request.GetMapping; import dev.plex.request.GetMapping;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -7,6 +8,11 @@ import jakarta.servlet.http.HttpServletResponse;
public class IndexEndpoint extends AbstractServlet public class IndexEndpoint extends AbstractServlet
{ {
public IndexEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "//") @GetMapping(endpoint = "//")
public String getIndex(HttpServletRequest request, HttpServletResponse response) public String getIndex(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -1,6 +1,7 @@
package dev.plex.request.impl; package dev.plex.request.impl;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import dev.plex.HTTPDModule;
import dev.plex.request.AbstractServlet; import dev.plex.request.AbstractServlet;
import dev.plex.request.GetMapping; import dev.plex.request.GetMapping;
import dev.plex.request.MappingHeaders; import dev.plex.request.MappingHeaders;
@@ -14,6 +15,11 @@ import org.bukkit.entity.Player;
public class ListEndpoint extends AbstractServlet public class ListEndpoint extends AbstractServlet
{ {
public ListEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/api/list/") @GetMapping(endpoint = "/api/list/")
@MappingHeaders(headers = "content-type;application/json") @MappingHeaders(headers = "content-type;application/json")
public String getOnlinePlayers(HttpServletRequest request, HttpServletResponse response) public String getOnlinePlayers(HttpServletRequest request, HttpServletResponse response)
@@ -23,6 +23,11 @@ public class PlayerAdminEndpoint extends AbstractServlet
{ {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z"); private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z");
public PlayerAdminEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/player/") @GetMapping(endpoint = "/player/")
public String getPlayer(HttpServletRequest request, HttpServletResponse response) public String getPlayer(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -54,15 +59,15 @@ public class PlayerAdminEndpoint extends AbstractServlet
return file; return file;
} }
private static PlexPlayerView lookupPlayer(String query) private PlexPlayerView lookupPlayer(String query)
{ {
try try
{ {
return HTTPDModule.plexApi().players().byUuid(UUID.fromString(query)).orElse(null); return module.api().players().byUuid(UUID.fromString(query)).orElse(null);
} }
catch (IllegalArgumentException ignored) catch (IllegalArgumentException ignored)
{ {
return HTTPDModule.plexApi().players().byName(query).orElse(null); return module.api().players().byName(query).orElse(null);
} }
} }
@@ -44,7 +44,6 @@ import org.bukkit.persistence.PersistentDataContainer;
*/ */
public final class PlayerInventoryBroadcaster public final class PlayerInventoryBroadcaster
{ {
private static final PlayerInventoryBroadcaster INSTANCE = new PlayerInventoryBroadcaster();
private static final long REFRESH_TICKS = 20L; // 1 second private static final long REFRESH_TICKS = 20L; // 1 second
private static final int MAX_NAME_CHARS = 256; private static final int MAX_NAME_CHARS = 256;
private static final int MAX_LORE_LINES = 20; private static final int MAX_LORE_LINES = 20;
@@ -53,11 +52,7 @@ public final class PlayerInventoryBroadcaster
private static final int MAX_PDC_KEYS = 64; private static final int MAX_PDC_KEYS = 64;
private static final int MAX_PDC_KEY_CHARS = 128; private static final int MAX_PDC_KEY_CHARS = 128;
public static PlayerInventoryBroadcaster get() private final HTTPDModule module;
{
return INSTANCE;
}
private final Map<UUID, Set<Subscriber>> subscribers = new ConcurrentHashMap<>(); private final Map<UUID, Set<Subscriber>> subscribers = new ConcurrentHashMap<>();
private final Map<UUID, String> cachedPayloads = new ConcurrentHashMap<>(); private final Map<UUID, String> cachedPayloads = new ConcurrentHashMap<>();
private final AtomicInteger subscriberCount = new AtomicInteger(); private final AtomicInteger subscriberCount = new AtomicInteger();
@@ -66,14 +61,17 @@ public final class PlayerInventoryBroadcaster
private ScheduledTask refreshTask; private ScheduledTask refreshTask;
private int maxConnections = 32; private int maxConnections = 32;
private PlayerInventoryBroadcaster() {} public PlayerInventoryBroadcaster(HTTPDModule module)
{
this.module = module;
}
public synchronized void start() public synchronized void start()
{ {
if (executor != null) return; if (executor != null) return;
maxConnections = HTTPDModule.moduleConfig.getInt("server.sse.max-connections", 32); maxConnections = module.getModuleConfig().getInt("server.sse.max-connections", 32);
int threads = Math.max(1, HTTPDModule.moduleConfig.getInt("server.sse.threads", 2)); int threads = Math.max(1, module.getModuleConfig().getInt("server.sse.threads", 2));
executor = Executors.newScheduledThreadPool(threads, r -> executor = Executors.newScheduledThreadPool(threads, r ->
{ {
@@ -84,11 +82,11 @@ public final class PlayerInventoryBroadcaster
try try
{ {
refreshTask = HTTPDModule.plexApi().scheduler().runGlobalTimer(this::tick, 1L, REFRESH_TICKS); refreshTask = module.api().scheduler().runGlobalTimer(this::tick, 1L, REFRESH_TICKS);
} }
catch (Throwable t) catch (Throwable t)
{ {
HTTPDModule.plexApi().logging().debug("PlayerInventoryBroadcaster: could not register refresh task: " + t.getMessage()); module.api().logging().debug("PlayerInventoryBroadcaster: could not register refresh task: " + t.getMessage());
} }
try try
@@ -97,7 +95,7 @@ public final class PlayerInventoryBroadcaster
} }
catch (Throwable t) catch (Throwable t)
{ {
HTTPDModule.plexApi().logging().debug("PlayerInventoryBroadcaster: NBT-API preload failed: " + t.getMessage()); module.api().logging().debug("PlayerInventoryBroadcaster: NBT-API preload failed: " + t.getMessage());
} }
} }
@@ -181,7 +179,7 @@ public final class PlayerInventoryBroadcaster
} }
try try
{ {
ScheduledTask task = HTTPDModule.plexApi().scheduler().runEntity(player, () -> ScheduledTask task = module.api().scheduler().runEntity(player, () ->
{ {
String json; String json;
try try
@@ -36,14 +36,9 @@ import java.util.concurrent.atomic.AtomicInteger;
*/ */
public final class PlayersBroadcaster public final class PlayersBroadcaster
{ {
private static final PlayersBroadcaster INSTANCE = new PlayersBroadcaster();
private static final long REFRESH_TICKS = 100L; // 5 seconds at 20 TPS private static final long REFRESH_TICKS = 100L; // 5 seconds at 20 TPS
public static PlayersBroadcaster get() private final HTTPDModule module;
{
return INSTANCE;
}
private final Set<Subscriber> subscribers = ConcurrentHashMap.newKeySet(); private final Set<Subscriber> subscribers = ConcurrentHashMap.newKeySet();
private final AtomicInteger subscriberCount = new AtomicInteger(); private final AtomicInteger subscriberCount = new AtomicInteger();
private final AtomicBoolean refreshScheduled = new AtomicBoolean(false); private final AtomicBoolean refreshScheduled = new AtomicBoolean(false);
@@ -56,14 +51,17 @@ public final class PlayersBroadcaster
private Listener listener; private Listener listener;
private int maxConnections = 32; private int maxConnections = 32;
private PlayersBroadcaster() {} public PlayersBroadcaster(HTTPDModule module)
{
this.module = module;
}
public synchronized void start() public synchronized void start()
{ {
if (executor != null) return; if (executor != null) return;
maxConnections = HTTPDModule.moduleConfig.getInt("server.sse.max-connections", 32); maxConnections = module.getModuleConfig().getInt("server.sse.max-connections", 32);
int threads = Math.max(1, HTTPDModule.moduleConfig.getInt("server.sse.threads", 2)); int threads = Math.max(1, module.getModuleConfig().getInt("server.sse.threads", 2));
executor = Executors.newScheduledThreadPool(threads, r -> executor = Executors.newScheduledThreadPool(threads, r ->
{ {
@@ -75,20 +73,20 @@ public final class PlayersBroadcaster
listener = new PlayersListener(); listener = new PlayersListener();
try try
{ {
HTTPDModule.plexApi().listeners().register(listener); module.api().listeners().register(listener);
} }
catch (Throwable t) catch (Throwable t)
{ {
HTTPDModule.plexApi().logging().debug("PlayersBroadcaster: could not register Bukkit listener: " + t.getMessage()); module.api().logging().debug("PlayersBroadcaster: could not register Bukkit listener: " + t.getMessage());
} }
try try
{ {
refreshTask = HTTPDModule.plexApi().scheduler().runGlobalTimer(this::refreshAndBroadcast, 1L, REFRESH_TICKS); refreshTask = module.api().scheduler().runGlobalTimer(this::refreshAndBroadcast, 1L, REFRESH_TICKS);
} }
catch (Throwable t) catch (Throwable t)
{ {
HTTPDModule.plexApi().logging().debug("PlayersBroadcaster: could not register refresh task: " + t.getMessage()); module.api().logging().debug("PlayersBroadcaster: could not register refresh task: " + t.getMessage());
} }
} }
@@ -173,7 +171,7 @@ public final class PlayersBroadcaster
Player player = online.get(i); Player player = online.get(i);
try try
{ {
ScheduledTask task = HTTPDModule.plexApi().scheduler().runEntity(player, () -> ScheduledTask task = module.api().scheduler().runEntity(player, () ->
{ {
try try
{ {
@@ -276,7 +274,7 @@ public final class PlayersBroadcaster
if (!refreshScheduled.compareAndSet(false, true)) return; if (!refreshScheduled.compareAndSet(false, true)) return;
try try
{ {
HTTPDModule.plexApi().scheduler().runGlobalLater(() -> module.api().scheduler().runGlobalLater(() ->
{ {
refreshScheduled.set(false); refreshScheduled.set(false);
refreshAndBroadcast(); refreshAndBroadcast();
@@ -1,5 +1,6 @@
package dev.plex.request.impl; package dev.plex.request.impl;
import dev.plex.HTTPDModule;
import dev.plex.request.AbstractServlet; import dev.plex.request.AbstractServlet;
import dev.plex.request.GetMapping; import dev.plex.request.GetMapping;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -7,6 +8,11 @@ import jakarta.servlet.http.HttpServletResponse;
public class PlayersEndpoint extends AbstractServlet public class PlayersEndpoint extends AbstractServlet
{ {
public PlayersEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/players/") @GetMapping(endpoint = "/players/")
public String getPlayers(HttpServletRequest request, HttpServletResponse response) public String getPlayers(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -16,6 +16,11 @@ import java.util.UUID;
public class PunishmentsEndpoint extends AbstractServlet public class PunishmentsEndpoint extends AbstractServlet
{ {
public PunishmentsEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/api/punishments/") @GetMapping(endpoint = "/api/punishments/")
public String getPunishments(HttpServletRequest request, HttpServletResponse response) public String getPunishments(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -28,11 +33,11 @@ public class PunishmentsEndpoint extends AbstractServlet
try try
{ {
UUID pathUUID = UUID.fromString(request.getPathInfo().replace("/", "")); UUID pathUUID = UUID.fromString(request.getPathInfo().replace("/", ""));
punishedPlayer = HTTPDModule.plexApi().players().byUuid(pathUUID).orElse(null); punishedPlayer = module.api().players().byUuid(pathUUID).orElse(null);
} }
catch (IllegalArgumentException ignored) catch (IllegalArgumentException ignored)
{ {
punishedPlayer = HTTPDModule.plexApi().players().byName(request.getPathInfo().replace("/", "")).orElse(null); punishedPlayer = module.api().players().byName(request.getPathInfo().replace("/", "")).orElse(null);
} }
if (punishedPlayer == null) if (punishedPlayer == null)
@@ -18,6 +18,11 @@ public class PunishmentsUIEndpoint extends AbstractServlet
{ {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z"); private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z");
public PunishmentsUIEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/punishments/") @GetMapping(endpoint = "/punishments/")
public String getPunishments(HttpServletRequest request, HttpServletResponse response) public String getPunishments(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -49,15 +54,15 @@ public class PunishmentsUIEndpoint extends AbstractServlet
return resultsHTML(punished, punishments, showIps); return resultsHTML(punished, punishments, showIps);
} }
private static PlexPlayerView lookupPlayer(String query) private PlexPlayerView lookupPlayer(String query)
{ {
try try
{ {
return HTTPDModule.plexApi().players().byUuid(UUID.fromString(query)).orElse(null); return module.api().players().byUuid(UUID.fromString(query)).orElse(null);
} }
catch (IllegalArgumentException ignored) catch (IllegalArgumentException ignored)
{ {
return HTTPDModule.plexApi().players().byName(query).orElse(null); return module.api().players().byName(query).orElse(null);
} }
} }
@@ -19,6 +19,11 @@ public class SchematicDownloadEndpoint extends AbstractServlet
{ {
List<File> files = new ArrayList<>(); List<File> files = new ArrayList<>();
public SchematicDownloadEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/api/schematics/download/") @GetMapping(endpoint = "/api/schematics/download/")
public String downloadSchematic(HttpServletRequest request, HttpServletResponse response) public String downloadSchematic(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -57,7 +62,7 @@ public class SchematicDownloadEndpoint extends AbstractServlet
{ {
try try
{ {
byte[] schemData = HTTPDModule.fileCache.getFile(schemFile); byte[] schemData = module.getFileCache().getFile(schemFile);
if (schemData != null) if (schemData != null)
{ {
outputStream.write(schemData); outputStream.write(schemData);
@@ -71,11 +76,11 @@ public class SchematicDownloadEndpoint extends AbstractServlet
} }
} }
private static void logDownload(HttpServletRequest request, File schemFile) private void logDownload(HttpServletRequest request, File schemFile)
{ {
AuthenticatedUser user = currentUser(request); AuthenticatedUser user = currentUser(request);
String who = user != null ? user.username() + " (xf:" + user.userId() + ")" : request.getRemoteAddr(); String who = user != null ? user.username() + " (xf:" + user.userId() + ")" : request.getRemoteAddr();
HTTPDModule.plexApi().logging().info("{0} downloaded schematic {1}", who, schemFile.getName()); module.api().logging().info("{0} downloaded schematic {1}", who, schemFile.getName());
Log.log("{0} downloaded schematic {1}", who, schemFile.getName()); Log.log("{0} downloaded schematic {1}", who, schemFile.getName());
} }
@@ -118,7 +123,7 @@ public class SchematicDownloadEndpoint extends AbstractServlet
{ {
if (fileEntry.isDirectory()) if (fileEntry.isDirectory())
{ {
HTTPDModule.plexApi().logging().debug("Found directory"); module.api().logging().debug("Found directory");
listFilesForFolder(fileEntry); listFilesForFolder(fileEntry);
} }
else else
@@ -1,5 +1,6 @@
package dev.plex.request.impl; package dev.plex.request.impl;
import dev.plex.HTTPDModule;
import dev.plex.authentication.AuthenticatedUser; import dev.plex.authentication.AuthenticatedUser;
import dev.plex.request.AbstractServlet; import dev.plex.request.AbstractServlet;
import dev.plex.request.GetMapping; import dev.plex.request.GetMapping;
@@ -8,6 +9,11 @@ import jakarta.servlet.http.HttpServletResponse;
public class SchematicUploadEndpoint extends AbstractServlet public class SchematicUploadEndpoint extends AbstractServlet
{ {
public SchematicUploadEndpoint(HTTPDModule module)
{
super(module);
}
@GetMapping(endpoint = "/api/schematics/upload/") @GetMapping(endpoint = "/api/schematics/upload/")
public String uploadSchematic(HttpServletRequest request, HttpServletResponse response) public String uploadSchematic(HttpServletRequest request, HttpServletResponse response)
{ {
@@ -27,13 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger;
*/ */
public final class StatsBroadcaster public final class StatsBroadcaster
{ {
private static final StatsBroadcaster INSTANCE = new StatsBroadcaster(); private final HTTPDModule module;
public static StatsBroadcaster get()
{
return INSTANCE;
}
private final Set<Subscriber> subscribers = ConcurrentHashMap.newKeySet(); private final Set<Subscriber> subscribers = ConcurrentHashMap.newKeySet();
private final AtomicInteger subscriberCount = new AtomicInteger(); private final AtomicInteger subscriberCount = new AtomicInteger();
@@ -58,15 +52,18 @@ public final class StatsBroadcaster
private int maxConnections = 32; private int maxConnections = 32;
private long broadcastIntervalMs = 2000L; private long broadcastIntervalMs = 2000L;
private StatsBroadcaster() {} public StatsBroadcaster(HTTPDModule module)
{
this.module = module;
}
public synchronized void start() public synchronized void start()
{ {
if (executor != null) return; if (executor != null) return;
maxConnections = HTTPDModule.moduleConfig.getInt("server.sse.max-connections", 32); maxConnections = module.getModuleConfig().getInt("server.sse.max-connections", 32);
broadcastIntervalMs = HTTPDModule.moduleConfig.getLong("server.sse.broadcast-interval-ms", 2000L); broadcastIntervalMs = module.getModuleConfig().getLong("server.sse.broadcast-interval-ms", 2000L);
int threads = Math.max(1, HTTPDModule.moduleConfig.getInt("server.sse.threads", 2)); int threads = Math.max(1, module.getModuleConfig().getInt("server.sse.threads", 2));
executor = Executors.newScheduledThreadPool(threads, r -> executor = Executors.newScheduledThreadPool(threads, r ->
{ {
@@ -77,11 +74,11 @@ public final class StatsBroadcaster
try try
{ {
bukkitTask = HTTPDModule.plexApi().scheduler().runGlobalTimer(this::sampleBukkit, 1L, 40L); bukkitTask = module.api().scheduler().runGlobalTimer(this::sampleBukkit, 1L, 40L);
} }
catch (Throwable t) catch (Throwable t)
{ {
HTTPDModule.plexApi().logging().debug("StatsBroadcaster: could not register Bukkit sampling task: " + t.getMessage()); module.api().logging().debug("StatsBroadcaster: could not register Bukkit sampling task: " + t.getMessage());
} }
broadcastTask = executor.scheduleAtFixedRate( broadcastTask = executor.scheduleAtFixedRate(