Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ repositories {
dependencies {
compileOnly 'org.slf4j:slf4j-api:1.7.36'
compileOnly 'net.luckperms:api:5.5'
implementation 'io.javalin:javalin:4.6.4'
implementation 'io.javalin:javalin-openapi:4.6.4'
implementation 'io.javalin:javalin:6.7.0'
implementation 'io.javalin.community.openapi:javalin-swagger-plugin:6.7.0-5'
}

shadowJar {
Expand Down
154 changes: 76 additions & 78 deletions src/main/java/me/lucko/luckperms/extension/rest/RestServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,10 @@
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.google.common.collect.ImmutableSet;
import io.javalin.Javalin;
import io.javalin.core.JavalinConfig;
import io.javalin.core.util.JavalinLogger;
import io.javalin.http.HttpCode;
import io.javalin.plugin.json.JavalinJackson;
import io.javalin.plugin.openapi.utils.OpenApiVersionUtil;
import io.javalin.config.JavalinConfig;
import io.javalin.http.HttpStatus;
import io.javalin.json.JavalinJackson;
import me.lucko.luckperms.extension.rest.controller.ActionController;
import me.lucko.luckperms.extension.rest.controller.EventController;
import me.lucko.luckperms.extension.rest.controller.GroupController;
Expand Down Expand Up @@ -70,77 +67,63 @@ public class RestServer implements AutoCloseable {

private final ObjectMapper objectMapper;
private final Javalin app;
private final AutoCloseable routesClosable;
private final EventController eventController;

public RestServer(LuckPerms luckPerms, String address, int port) {
LOGGER.info("[REST] Starting server...");

this.objectMapper = new CustomObjectMapper();

this.app = Javalin.create(this::configure)
MessagingService messagingService = luckPerms.getMessagingService().orElse(StubMessagingService.INSTANCE);

UserController userController = new UserController(luckPerms.getUserManager(), luckPerms.getTrackManager(), messagingService, this.objectMapper);
GroupController groupController = new GroupController(luckPerms.getGroupManager(), messagingService, this.objectMapper);
TrackController trackController = new TrackController(luckPerms.getTrackManager(), luckPerms.getGroupManager(), messagingService, this.objectMapper);
ActionController actionController = new ActionController(luckPerms.getActionLogger(), this.objectMapper);
MessagingController messagingController = new MessagingController(luckPerms.getMessagingService().orElse(null), luckPerms.getUserManager(), this.objectMapper);
this.eventController = new EventController(luckPerms.getEventBus());

this.app = Javalin.create(config -> this.configure(config, luckPerms, userController, groupController, trackController, actionController, messagingController, this.eventController))
.start(address, port);

this.setupLogging(this.app);
this.setupErrorHandlers(this.app);
this.routesClosable = this.setupRoutes(this.app, luckPerms);

LOGGER.info("[REST] Startup complete! Listening on http://{}:{}", address == null ? "localhost" : address, port);
}

@Override
public void close() {
try {
this.routesClosable.close();
this.eventController.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
this.app.close();
this.app.stop();
}

private void configure(JavalinConfig config) {
private void configure(JavalinConfig config, LuckPerms luckPerms, UserController userController, GroupController groupController, TrackController trackController, ActionController actionController, MessagingController messagingController, EventController eventController) {
// disable javalin excessive logging
config.showJavalinBanner = false;
JavalinLogger.enabled = false;
JavalinLogger.startupInfo = false;
OpenApiVersionUtil.INSTANCE.setLogWarnings(false);

// Enable webjars for Swagger UI assets
config.staticFiles.enableWebjars();

this.setupAuth(config);

SwaggerUi.setup(config);

config.jsonMapper(new JavalinJackson(this.objectMapper));
}

private void setupErrorHandlers(Javalin app) {
app.exception(MismatchedInputException.class, (e, ctx) -> ctx.status(400).result(e.getMessage()));
app.exception(JacksonException.class, (e, ctx) -> ctx.status(400).result(e.getMessage()));
app.exception(IllegalArgumentException.class, (e, ctx) -> ctx.status(400).result(e.getMessage()));
app.exception(UnsupportedOperationException.class, (e, ctx) -> ctx.status(404).result("Not found"));

app.exception(Exception.class, (e, ctx) -> {
ctx.status(500).result("Server error");
LOGGER.error("Server error while handing request", e);
});
}
config.jsonMapper(new JavalinJackson(this.objectMapper, true));

private AutoCloseable setupRoutes(Javalin app, LuckPerms luckPerms) {
app.get("/", ctx -> ctx.redirect("/docs/swagger-ui"));
// Setup routes
config.router.apiBuilder(() -> {
get("/", ctx -> ctx.redirect("/docs/swagger-ui"));

app.get("health", ctx -> {
Health health = luckPerms.runHealthCheck();
ctx.status(health.isHealthy() ? HttpCode.OK : HttpCode.SERVICE_UNAVAILABLE).json(health);
});

MessagingService messagingService = luckPerms.getMessagingService().orElse(StubMessagingService.INSTANCE);

UserController userController = new UserController(luckPerms.getUserManager(), luckPerms.getTrackManager(), messagingService, this.objectMapper);
GroupController groupController = new GroupController(luckPerms.getGroupManager(), messagingService, this.objectMapper);
TrackController trackController = new TrackController(luckPerms.getTrackManager(), luckPerms.getGroupManager(), messagingService, this.objectMapper);
ActionController actionController = new ActionController(luckPerms.getActionLogger(), this.objectMapper);
MessagingController messagingController = new MessagingController(luckPerms.getMessagingService().orElse(null), luckPerms.getUserManager(), this.objectMapper);
EventController eventController = new EventController(luckPerms.getEventBus());
get("health", ctx -> {
Health health = luckPerms.runHealthCheck();
ctx.status(health.isHealthy() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE).json(health);
});

app.routes(() -> {
path("user", () -> {
get("lookup", userController::lookup);
setupControllerRoutes(userController);
Expand All @@ -151,8 +134,18 @@ private AutoCloseable setupRoutes(Javalin app, LuckPerms luckPerms) {
path("messaging", () -> setupControllerRoutes(messagingController));
path("event", () -> setupControllerRoutes(eventController));
});
}

return eventController;
private void setupErrorHandlers(Javalin app) {
app.exception(MismatchedInputException.class, (e, ctx) -> ctx.status(400).result(e.getMessage()));
app.exception(JacksonException.class, (e, ctx) -> ctx.status(400).result(e.getMessage()));
app.exception(IllegalArgumentException.class, (e, ctx) -> ctx.status(400).result(e.getMessage()));
app.exception(UnsupportedOperationException.class, (e, ctx) -> ctx.status(404).result("Not found"));

app.exception(Exception.class, (e, ctx) -> {
ctx.status(500).result("Server error");
LOGGER.error("Server error while handing request", e);
});
}

private void setupControllerRoutes(PermissionHolderController controller) {
Expand Down Expand Up @@ -225,7 +218,7 @@ private void setupControllerRoutes(EventController controller) {

private void setupAuth(JavalinConfig config) {
if (RestConfig.getBoolean("auth", false)) {
Set<String> keys = ImmutableSet.copyOf(
Set<String> keys = Set.copyOf(
RestConfig.getStringList("auth.keys", Collections.emptyList())
);

Expand All @@ -234,35 +227,40 @@ private void setupAuth(JavalinConfig config) {
LOGGER.warn("[REST] Set some keys with the 'LUCKPERMS_REST_AUTH_KEYS' variable.");
}

config.accessManager((handler, ctx, routeRoles) -> {
if (ctx.path().equals("/") || ctx.path().startsWith("/docs")) {
handler.handle(ctx);
return;
}

String authorization = ctx.header("Authorization");
if (authorization == null) {
ctx.status(HttpCode.UNAUTHORIZED).result("No API key");
return;
}

String[] parts = authorization.split(" ");
if (parts.length != 2) {
ctx.status(HttpCode.UNAUTHORIZED).result("Invalid API key");
return;
}

if (!parts[0].equals("Bearer")) {
ctx.status(HttpCode.UNAUTHORIZED).result("Unknown Authorization type");
return;
}

if (!keys.contains(parts[1])) {
ctx.status(HttpCode.UNAUTHORIZED).result("Unauthorized");
return;
}

handler.handle(ctx);
config.router.mount(router -> {
router.beforeMatched(ctx -> {
// Skip auth for root, docs, and webjars (Swagger UI assets)
String path = ctx.path();
if (path.equals("/") || path.startsWith("/docs") || path.startsWith("/webjars")) {
return;
}

String authorization = ctx.header("Authorization");
if (authorization == null) {
ctx.status(HttpStatus.UNAUTHORIZED).result("No API key");
ctx.skipRemainingHandlers();
return;
}

String[] parts = authorization.split(" ");
if (parts.length != 2) {
ctx.status(HttpStatus.UNAUTHORIZED).result("Invalid API key");
ctx.skipRemainingHandlers();
return;
}

if (!parts[0].equals("Bearer")) {
ctx.status(HttpStatus.UNAUTHORIZED).result("Unknown Authorization type");
ctx.skipRemainingHandlers();
return;
}

if (!keys.contains(parts[1])) {
ctx.status(HttpStatus.UNAUTHORIZED).result("Unauthorized");
ctx.skipRemainingHandlers();
return;
}
});
});
}
}
Expand All @@ -271,14 +269,14 @@ private void setupLogging(Javalin app) {
app.before(ctx -> {
ctx.attribute("startTime", System.currentTimeMillis());
if (ctx.path().startsWith("/event/")) {
LOGGER.info("[REST] %s %s - %d".formatted(ctx.method(), ctx.path(), ctx.status()));
LOGGER.info("[REST] {} {} - {}", ctx.method(), ctx.path(), ctx.status());
}
});
app.after(ctx -> {
//noinspection ConstantConditions
long startTime = ctx.attribute("startTime");
long duration = System.currentTimeMillis() - startTime;
LOGGER.info("[REST] %s %s - %d - %dms".formatted(ctx.method(), ctx.path(), ctx.status(), duration));
LOGGER.info("[REST] {} {} - {} - {}ms", ctx.method(), ctx.path(), ctx.status(), duration);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void get(Context ctx) throws JsonProcessingException {
if (pageSize == null && pageNumber == null) {
CompletableFuture<ActionPage> future = this.actionLogger.queryActions(filter)
.thenApply(list -> new ActionPage(list, list.size()));
ctx.future(future);
ctx.future(() -> future.thenAccept(ctx::json));
} else {
if (pageSize == null) {
ctx.status(400).result("pageSize query parameter is required when pageNumber is provided");
Expand All @@ -68,7 +68,7 @@ public void get(Context ctx) throws JsonProcessingException {

CompletableFuture<ActionPage> future = this.actionLogger.queryActions(filter, pageSize, pageNumber)
.thenApply(ActionPage::from);
ctx.future(future);
ctx.future(() -> future.thenAccept(ctx::json));
}
}

Expand All @@ -77,7 +77,7 @@ public void submit(Context ctx) {
Action req = ctx.bodyAsClass(Action.class);

CompletableFuture<Void> future = this.actionLogger.submit(req);
ctx.future(future, result -> ctx.status(202).result("ok"));
ctx.future(() -> future.thenAccept(result -> ctx.status(202).result("ok")));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

package me.lucko.luckperms.extension.rest.controller;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.javalin.http.sse.SseClient;
import net.luckperms.api.event.EventBus;
import net.luckperms.api.event.EventSubscription;
Expand All @@ -38,11 +37,12 @@
import net.luckperms.api.event.sync.PreSyncEvent;

import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

public class EventController implements AutoCloseable {
Expand All @@ -58,9 +58,14 @@ public EventController(EventBus eventBus) {
this.clients = ConcurrentHashMap.newKeySet();
this.pingCounter = new AtomicLong();

this.executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder()
.setNameFormat("luckperms-rest-event-controller-%d")
.build());
AtomicInteger threadCount = new AtomicInteger(0);
ThreadFactory threadFactory = r -> {
Thread thread = new Thread(r);
thread.setName("luckperms-rest-event-controller-" + threadCount.getAndIncrement());
thread.setDaemon(true);
return thread;
};
this.executor = Executors.newSingleThreadScheduledExecutor(threadFactory);
this.executor.scheduleAtFixedRate(this::tick, 10, 10, TimeUnit.SECONDS);
}

Expand All @@ -81,14 +86,13 @@ public void close() throws Exception {

private void handle(SseClient client, Class<? extends LuckPermsEvent> eventClass) {
this.clients.add(client);
CompletableFuture<Object> future = new CompletableFuture<>();
// Keep the SSE connection alive
client.keepAlive();
EventSubscription<?> subscription = this.eventBus.subscribe(eventClass, client::sendEvent);
client.onClose(() -> {
future.complete(null);
subscription.close();
this.clients.remove(client);
});
client.ctx.future(future);

}

Expand Down
Loading