package emu.grasscutter.server.http; import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.utils.FileUtils; import express.Express; import express.http.MediaType; import io.javalin.Javalin; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; import java.io.File; import java.io.UnsupportedEncodingException; import static emu.grasscutter.config.Configuration.*; import static emu.grasscutter.utils.Language.translate; /** * Manages all HTTP-related classes. * (including dispatch, announcements, gacha, etc.) */ public final class HttpServer { private final Express express; /** * Configures the Express application. */ public HttpServer() { this.express = new Express(config -> { // Set the Express HTTP server. config.server(HttpServer::createServer); // Configure encryption/HTTPS/SSL. config.enforceSsl = HTTP_ENCRYPTION.useEncryption; // Configure HTTP policies. if (HTTP_POLICIES.cors.enabled) { var allowedOrigins = HTTP_POLICIES.cors.allowedOrigins; if (allowedOrigins.length > 0) config.enableCorsForOrigin(allowedOrigins); else config.enableCorsForAllOrigins(); } // Configure debug logging. if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL) config.enableDevLogging(); // Disable compression on static files. config.precompressStaticFiles = false; }); } /** * Creates an HTTP(S) server. * @return A server instance. */ @SuppressWarnings("resource") private static Server createServer() { Server server = new Server(); ServerConnector serverConnector = new ServerConnector(server); if (HTTP_ENCRYPTION.useEncryption) { var sslContextFactory = new SslContextFactory.Server(); var keystoreFile = new File(HTTP_ENCRYPTION.keystore); if (!keystoreFile.exists()) { HTTP_ENCRYPTION.useEncryption = false; HTTP_ENCRYPTION.useInRouting = false; Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); } else try { sslContextFactory.setKeyStorePath(keystoreFile.getPath()); sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword); } catch (Exception ignored) { Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error")); try { sslContextFactory.setKeyStorePath(keystoreFile.getPath()); sslContextFactory.setKeyStorePassword("123456"); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password")); } catch (Exception exception) { Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error"), exception); } } finally { serverConnector = new ServerConnector(server, sslContextFactory); } } serverConnector.setPort(HTTP_INFO.bindPort); server.setConnectors(new ServerConnector[]{serverConnector}); return server; } /** * Returns the handle for the Express application. * @return A Javalin instance. */ public Javalin getHandle() { return this.express.raw(); } /** * Initializes the provided class. * @param router The router class. * @return Method chaining. */ @SuppressWarnings("UnusedReturnValue") public HttpServer addRouter(Class router, Object... args) { // Get all constructor parameters. Class[] types = new Class[args.length]; for (var argument : args) types[args.length - 1] = argument.getClass(); try { // Create a router instance & apply routes. var constructor = router.getDeclaredConstructor(types); // Get the constructor. var routerInstance = constructor.newInstance(args); // Create instance. routerInstance.applyRoutes(this.express, this.getHandle()); // Apply routes. } catch (Exception exception) { Grasscutter.getLogger().warn(translate("messages.dispatch.router_error"), exception); } return this; } /** * Starts listening on the HTTP server. * @throws UnsupportedEncodingException */ public void start() throws UnsupportedEncodingException { // Attempt to start the HTTP server. if (HTTP_INFO.bindAddress.equals("")) { this.express.listen(HTTP_INFO.bindPort); }else { this.express.listen(HTTP_INFO.bindAddress, HTTP_INFO.bindPort); } // Log bind information. Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(this.express.raw().port()))); } /** * Handles the '/' (index) endpoint on the Express application. */ public static class DefaultRequestRouter implements Router { @Override public void applyRoutes(Express express, Javalin handle) { express.get("/", (request, response) -> { File file = new File(HTTP_STATIC_FILES.indexFile); if (!file.exists()) response.send(""" %s """.formatted(translate("messages.status.welcome"))); else { final var filePath = file.getPath(); final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1)); response.type((fromExtension != null) ? fromExtension.getMIME() : "text/plain") .send(FileUtils.read(filePath)); } }); } } /** * Handles unhandled endpoints on the Express application. */ public static class UnhandledRequestRouter implements Router { @Override public void applyRoutes(Express express, Javalin handle) { handle.error(404, context -> { if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING) Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url())); context.contentType("text/html"); File file = new File(HTTP_STATIC_FILES.errorFile); if (!file.exists()) context.result(""" """); else { final var filePath = file.getPath(); final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1)); context.contentType((fromExtension != null) ? fromExtension.getMIME() : "text/plain") .result(FileUtils.read(filePath)); } }); } } }