package xyz.etztech.serverapi.web; import io.javalin.Javalin; import io.javalin.core.event.EventListener; import io.javalin.core.plugin.Plugin; import io.javalin.core.util.RouteOverviewPlugin; import io.javalin.http.BadRequestResponse; import io.javalin.http.Context; import io.javalin.http.UnauthorizedResponse; import io.javalin.plugin.graphql.GraphQLOptions; import io.javalin.plugin.graphql.GraphQLPlugin; import io.javalin.plugin.json.JavalinJson; import org.apache.commons.lang.exception.ExceptionUtils; import org.slf4j.helpers.NOPLogger; import xyz.etztech.serverapi.ServerAPI; import xyz.etztech.serverapi.token.TokenList; import xyz.etztech.serverapi.web.api.BanAPI; import xyz.etztech.serverapi.web.api.BroadcastAPI; import xyz.etztech.serverapi.web.api.CustomAPI; import xyz.etztech.serverapi.web.api.ErrorAPI; import java.util.List; public class Web { private final IProvider provider; private TokenList tokens; private List custom; private Javalin app; public Web(IProvider provider) { this.provider = provider; } public void start(int port, TokenList tokens, List custom) { Javalin.log = NOPLogger.NOP_LOGGER; app = Javalin.create(config -> { config.registerPlugin(graphql()); config.registerPlugin(new RouteOverviewPlugin("/")); config.enableCorsForAllOrigins(); }).events(this::events).exception(Exception.class, this::exception); this.custom = custom; this.tokens = tokens; if (tokens != null) { provider.log(String.format("Loaded %d tokens", tokens.getTokens().size())); if (!tokens.isProtectPOST()) { provider.log("WARNING: You have disabled POST protection, which can enable users to arbitrarily run actions against your server."); } app.before(this::access); } // REST Endpoints // For GraphQL endpoints, see GraphQL.java app.get("/bans", this::bans); app.get("/players", this::players); app.get("/plugins", this::plugins); app.get("/ping", this::ping); app.get("/tps", this::tps); app.get("/worlds", this::worlds); app.get("/worlds/:name", this::world); app.get("/chat", this::chat); app.post("/kick", this::kick); app.post("/ban", this::ban); app.post("/unban", this::unban); app.post("/broadcast", this::broadcast); app.post("/custom", this::custom); // What in the actual fuck... https://github.com/tipsy/javalin/issues/358 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(ServerAPI.class.getClassLoader()); app.start(port); Thread.currentThread().setContextClassLoader(classLoader); } public void stop() { if (app != null) { app.stop(); } } public void events(EventListener event) { event.serverStarting(() -> provider.log("Starting API...")); event.serverStarted(() -> provider.log("API is ready!")); event.serverStartFailed(() -> provider.log("Could not start API...")); event.serverStopping(() -> provider.log("Stopping API...")); event.serverStopped(() -> provider.log("API is stopped!")); } public void exception(Exception exception, Context ctx) { provider.log(String.format("API Exception at %s:\n%s", ctx.req.getRequestURI(), ExceptionUtils.getStackTrace(exception))); ctx.status(500); ctx.json(new ErrorAPI(500, "Internal Error")); } public void access(Context ctx) { String token = ctx.header("X-ServerAPI-Token"); if (token == null) { token = ctx.queryParam("token"); } boolean isGET = "GET".equalsIgnoreCase(ctx.method()); boolean isPOST = "POST".equalsIgnoreCase(ctx.method()); if (isGET && !tokens.isProtectGET()) return; if (isPOST && !tokens.isProtectPOST()) return; String finalToken = token; boolean pass = tokens.getTokens().stream().anyMatch(t -> t.getToken().equals(finalToken) && ((isGET && t.canGET()) || (isPOST && t.canPOST()))); if (!pass) { throw new UnauthorizedResponse(); } } public Plugin graphql() { GraphQLOptions options = new GraphQLOptions("/graphql", null) .addPackage("xyz.etztech.serverapi.web.api") .register(new GraphQL(provider)); return new GraphQLPlugin(options); } public void tps(Context ctx) { ctx.json(provider.TPS()); } public void worlds(Context ctx) { ctx.json(provider.worlds()); } public void world(Context ctx) { ctx.json(provider.world(ctx.pathParam("name"))); } public void chat(Context ctx) { ctx.json(provider.chat()); } public void players(Context ctx) { ctx.json(provider.players()); } public void plugins(Context ctx) { ctx.json(provider.plugins()); } public void bans(Context ctx) { ctx.json(provider.bans()); } public void ping(Context ctx) { ctx.json(provider.ping()); } public void kick(Context ctx) { provider.kick(JavalinJson.fromJson(ctx.body(), BanAPI.class)); ctx.status(200); } public void ban(Context ctx) { provider.ban(JavalinJson.fromJson(ctx.body(), BanAPI.class)); ctx.status(200); } public void unban(Context ctx) { provider.unban(JavalinJson.fromJson(ctx.body(), BanAPI.class)); ctx.status(200); } public void broadcast(Context ctx) { provider.broadcast(JavalinJson.fromJson(ctx.body(), BroadcastAPI.class)); ctx.status(200); } public void custom(Context ctx) { CustomAPI capi = JavalinJson.fromJson(ctx.body(), CustomAPI.class); if (custom.stream().noneMatch(c -> c.equalsIgnoreCase(capi.getCommand()))) { throw new BadRequestResponse(); } provider.custom(capi); ctx.status(200); } }