Commit b5bed6ce authored by Benj's avatar Benj Committed by Melledy
Browse files

Update HttpServer & AuthenticationSystem to use Javalin

parent bf960622
...@@ -94,6 +94,7 @@ dependencies { ...@@ -94,6 +94,7 @@ dependencies {
implementation group: 'com.github.davidmoten', name : 'rtree-multi', version: '0.1' implementation group: 'com.github.davidmoten', name : 'rtree-multi', version: '0.1'
implementation group: 'io.javalin', name: 'javalin', version: '4.6.4' implementation group: 'io.javalin', name: 'javalin', version: '4.6.4'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.3'
protobuf files('proto/') protobuf files('proto/')
......
...@@ -2,8 +2,7 @@ package emu.grasscutter.auth; ...@@ -2,8 +2,7 @@ package emu.grasscutter.auth;
import emu.grasscutter.game.Account; import emu.grasscutter.game.Account;
import emu.grasscutter.server.http.objects.*; import emu.grasscutter.server.http.objects.*;
import express.http.Request; import io.javalin.http.Context;
import express.http.Response;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
...@@ -15,7 +14,7 @@ import javax.annotation.Nullable; ...@@ -15,7 +14,7 @@ import javax.annotation.Nullable;
* Can be changed by plugins. * Can be changed by plugins.
*/ */
public interface AuthenticationSystem { public interface AuthenticationSystem {
/** /**
* Called when a user requests to make an account. * Called when a user requests to make an account.
* @param username The provided username. * @param username The provided username.
...@@ -71,9 +70,8 @@ public interface AuthenticationSystem { ...@@ -71,9 +70,8 @@ public interface AuthenticationSystem {
*/ */
@Builder @AllArgsConstructor @Getter @Builder @AllArgsConstructor @Getter
class AuthenticationRequest { class AuthenticationRequest {
private final Request request; private final Context context;
@Nullable private final Response response;
@Nullable private final LoginAccountRequestJson passwordRequest; @Nullable private final LoginAccountRequestJson passwordRequest;
@Nullable private final LoginTokenRequestJson tokenRequest; @Nullable private final LoginTokenRequestJson tokenRequest;
@Nullable private final ComboTokenReqJson sessionKeyRequest; @Nullable private final ComboTokenReqJson sessionKeyRequest;
...@@ -82,53 +80,51 @@ public interface AuthenticationSystem { ...@@ -82,53 +80,51 @@ public interface AuthenticationSystem {
/** /**
* Generates an authentication request from a {@link LoginAccountRequestJson} object. * Generates an authentication request from a {@link LoginAccountRequestJson} object.
* @param request The Express request. * @param ctx The Javalin context.
* @param jsonData The JSON data. * @param jsonData The JSON data.
* @return An authentication request. * @return An authentication request.
*/ */
static AuthenticationRequest fromPasswordRequest(Request request, LoginAccountRequestJson jsonData) { static AuthenticationRequest fromPasswordRequest(Context ctx, LoginAccountRequestJson jsonData) {
return AuthenticationRequest.builder() return AuthenticationRequest.builder()
.request(request) .context(ctx)
.passwordRequest(jsonData) .passwordRequest(jsonData)
.build(); .build();
} }
/** /**
* Generates an authentication request from a {@link LoginTokenRequestJson} object. * Generates an authentication request from a {@link LoginTokenRequestJson} object.
* @param request The Express request. * @param ctx The Javalin context.
* @param jsonData The JSON data. * @param jsonData The JSON data.
* @return An authentication request. * @return An authentication request.
*/ */
static AuthenticationRequest fromTokenRequest(Request request, LoginTokenRequestJson jsonData) { static AuthenticationRequest fromTokenRequest(Context ctx, LoginTokenRequestJson jsonData) {
return AuthenticationRequest.builder() return AuthenticationRequest.builder()
.request(request) .context(ctx)
.tokenRequest(jsonData) .tokenRequest(jsonData)
.build(); .build();
} }
/** /**
* Generates an authentication request from a {@link ComboTokenReqJson} object. * Generates an authentication request from a {@link ComboTokenReqJson} object.
* @param request The Express request. * @param ctx The Javalin context.
* @param jsonData The JSON data. * @param jsonData The JSON data.
* @return An authentication request. * @return An authentication request.
*/ */
static AuthenticationRequest fromComboTokenRequest(Request request, ComboTokenReqJson jsonData, static AuthenticationRequest fromComboTokenRequest(Context ctx, ComboTokenReqJson jsonData,
ComboTokenReqJson.LoginTokenData tokenData) { ComboTokenReqJson.LoginTokenData tokenData) {
return AuthenticationRequest.builder() return AuthenticationRequest.builder()
.request(request) .context(ctx)
.sessionKeyRequest(jsonData) .sessionKeyRequest(jsonData)
.sessionKeyData(tokenData) .sessionKeyData(tokenData)
.build(); .build();
} }
/** /**
* Generates an authentication request from a {@link Response} object. * Generates an authentication request from a {@link Context} object.
* @param request The Express request. * @param ctx The Javalin context.
* @param response the Express response.
* @return An authentication request. * @return An authentication request.
*/ */
static AuthenticationRequest fromExternalRequest(Request request, Response response) { static AuthenticationRequest fromExternalRequest(Context ctx) {
return AuthenticationRequest.builder().request(request) return AuthenticationRequest.builder().context(ctx).build();
.response(response).build();
} }
} }
...@@ -36,7 +36,7 @@ public final class DefaultAuthenticators { ...@@ -36,7 +36,7 @@ public final class DefaultAuthenticators {
int playerCount = Grasscutter.getGameServer().getPlayers().size(); int playerCount = Grasscutter.getGameServer().getPlayers().size();
boolean successfulLogin = false; boolean successfulLogin = false;
String address = request.getRequest().ip(); String address = request.getContext().ip();
String responseMessage = translate("messages.dispatch.account.username_error"); String responseMessage = translate("messages.dispatch.account.username_error");
String loggerMessage = ""; String loggerMessage = "";
...@@ -99,7 +99,7 @@ public final class DefaultAuthenticators { ...@@ -99,7 +99,7 @@ public final class DefaultAuthenticators {
int playerCount = Grasscutter.getGameServer().getPlayers().size(); int playerCount = Grasscutter.getGameServer().getPlayers().size();
boolean successfulLogin = false; boolean successfulLogin = false;
String address = request.getRequest().ip(); String address = request.getContext().ip();
String responseMessage = translate("messages.dispatch.account.username_error"); String responseMessage = translate("messages.dispatch.account.username_error");
String loggerMessage = ""; String loggerMessage = "";
String decryptedPassword = ""; String decryptedPassword = "";
...@@ -205,7 +205,7 @@ public final class DefaultAuthenticators { ...@@ -205,7 +205,7 @@ public final class DefaultAuthenticators {
assert requestData != null; assert requestData != null;
boolean successfulLogin; boolean successfulLogin;
String address = request.getRequest().ip(); String address = request.getContext().ip();
String loggerMessage; String loggerMessage;
int playerCount = Grasscutter.getGameServer().getPlayers().size(); int playerCount = Grasscutter.getGameServer().getPlayers().size();
...@@ -263,7 +263,7 @@ public final class DefaultAuthenticators { ...@@ -263,7 +263,7 @@ public final class DefaultAuthenticators {
assert loginData != null; assert loginData != null;
boolean successfulLogin; boolean successfulLogin;
String address = request.getRequest().ip(); String address = request.getContext().ip();
String loggerMessage; String loggerMessage;
int playerCount = Grasscutter.getGameServer().getPlayers().size(); int playerCount = Grasscutter.getGameServer().getPlayers().size();
...@@ -309,43 +309,37 @@ public final class DefaultAuthenticators { ...@@ -309,43 +309,37 @@ public final class DefaultAuthenticators {
public static class ExternalAuthentication implements ExternalAuthenticator { public static class ExternalAuthentication implements ExternalAuthenticator {
@Override @Override
public void handleLogin(AuthenticationRequest request) { public void handleLogin(AuthenticationRequest request) {
assert request.getResponse() != null; request.getContext().result("Authentication is not available with the default authentication method.");
request.getResponse().send("Authentication is not available with the default authentication method.");
} }
@Override @Override
public void handleAccountCreation(AuthenticationRequest request) { public void handleAccountCreation(AuthenticationRequest request) {
assert request.getResponse() != null; request.getContext().result("Authentication is not available with the default authentication method.");
request.getResponse().send("Authentication is not available with the default authentication method.");
} }
@Override @Override
public void handlePasswordReset(AuthenticationRequest request) { public void handlePasswordReset(AuthenticationRequest request) {
assert request.getResponse() != null; request.getContext().result("Authentication is not available with the default authentication method.");
request.getResponse().send("Authentication is not available with the default authentication method.");
} }
} }
/** /**
* Handles authentication requests from OAuth sources. * Handles authentication requests from OAuth sources.Zenlith
*/ */
public static class OAuthAuthentication implements OAuthAuthenticator { public static class OAuthAuthentication implements OAuthAuthenticator {
@Override @Override
public void handleLogin(AuthenticationRequest request) { public void handleLogin(AuthenticationRequest request) {
assert request.getResponse() != null; request.getContext().result("Authentication is not available with the default authentication method.");
request.getResponse().send("Authentication is not available with the default authentication method.");
} }
@Override @Override
public void handleRedirection(AuthenticationRequest request, ClientType type) { public void handleRedirection(AuthenticationRequest request, ClientType type) {
assert request.getResponse() != null; request.getContext().result("Authentication is not available with the default authentication method.");
request.getResponse().send("Authentication is not available with the default authentication method.");
} }
@Override @Override
public void handleTokenProcess(AuthenticationRequest request) { public void handleTokenProcess(AuthenticationRequest request) {
assert request.getResponse() != null; request.getContext().result("Authentication is not available with the default authentication method.");
request.getResponse().send("Authentication is not available with the default authentication method.");
} }
} }
} }
...@@ -3,9 +3,9 @@ package emu.grasscutter.server.http; ...@@ -3,9 +3,9 @@ package emu.grasscutter.server.http;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import express.Express; import emu.grasscutter.utils.HttpUtils;
import express.http.MediaType;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.core.util.JavalinLogger;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
...@@ -21,14 +21,14 @@ import static emu.grasscutter.utils.Language.translate; ...@@ -21,14 +21,14 @@ import static emu.grasscutter.utils.Language.translate;
* (including dispatch, announcements, gacha, etc.) * (including dispatch, announcements, gacha, etc.)
*/ */
public final class HttpServer { public final class HttpServer {
private final Express express; private final Javalin javalin;
/** /**
* Configures the Express application. * Configures the Javalin application.
*/ */
public HttpServer() { public HttpServer() {
this.express = new Express(config -> { this.javalin = Javalin.create(config -> {
// Set the Express HTTP server. // Set the Javalin HTTP server.
config.server(HttpServer::createServer); config.server(HttpServer::createServer);
// Configure encryption/HTTPS/SSL. // Configure encryption/HTTPS/SSL.
...@@ -46,8 +46,7 @@ public final class HttpServer { ...@@ -46,8 +46,7 @@ public final class HttpServer {
if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL) if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL)
config.enableDevLogging(); config.enableDevLogging();
// Disable compression on static files. // Static files should be added like this https://javalin.io/documentation#static-files
config.precompressStaticFiles = false;
}); });
} }
...@@ -100,7 +99,7 @@ public final class HttpServer { ...@@ -100,7 +99,7 @@ public final class HttpServer {
* @return A Javalin instance. * @return A Javalin instance.
*/ */
public Javalin getHandle() { public Javalin getHandle() {
return this.express.raw(); return this.javalin;
} }
/** /**
...@@ -118,7 +117,7 @@ public final class HttpServer { ...@@ -118,7 +117,7 @@ public final class HttpServer {
try { // Create a router instance & apply routes. try { // Create a router instance & apply routes.
var constructor = router.getDeclaredConstructor(types); // Get the constructor. var constructor = router.getDeclaredConstructor(types); // Get the constructor.
var routerInstance = constructor.newInstance(args); // Create instance. var routerInstance = constructor.newInstance(args); // Create instance.
routerInstance.applyRoutes(this.express, this.getHandle()); // Apply routes. routerInstance.applyRoutes(this.javalin); // Apply routes.
} catch (Exception exception) { } catch (Exception exception) {
Grasscutter.getLogger().warn(translate("messages.dispatch.router_error"), exception); Grasscutter.getLogger().warn(translate("messages.dispatch.router_error"), exception);
} return this; } return this;
...@@ -131,24 +130,24 @@ public final class HttpServer { ...@@ -131,24 +130,24 @@ public final class HttpServer {
public void start() throws UnsupportedEncodingException { public void start() throws UnsupportedEncodingException {
// Attempt to start the HTTP server. // Attempt to start the HTTP server.
if (HTTP_INFO.bindAddress.equals("")) { if (HTTP_INFO.bindAddress.equals("")) {
this.express.listen(HTTP_INFO.bindPort); this.javalin.start(HTTP_INFO.bindPort);
}else { }else {
this.express.listen(HTTP_INFO.bindAddress, HTTP_INFO.bindPort); this.javalin.start(HTTP_INFO.bindAddress, HTTP_INFO.bindPort);
} }
// Log bind information. // Log bind information.
Grasscutter.getLogger().info(translate("messages.dispatch.address_bind", HTTP_INFO.accessAddress, this.express.raw().port())); Grasscutter.getLogger().info(translate("messages.dispatch.address_bind", HTTP_INFO.accessAddress, this.javalin.port()));
} }
/** /**
* Handles the '/' (index) endpoint on the Express application. * Handles the '/' (index) endpoint on the Express application.
*/ */
public static class DefaultRequestRouter implements Router { public static class DefaultRequestRouter implements Router {
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
express.get("/", (request, response) -> { javalin.get("/", ctx -> {
File file = new File(HTTP_STATIC_FILES.indexFile); File file = new File(HTTP_STATIC_FILES.indexFile);
if (!file.exists()) if (!file.exists())
response.send(""" ctx.result("""
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
...@@ -159,9 +158,8 @@ public final class HttpServer { ...@@ -159,9 +158,8 @@ public final class HttpServer {
""".formatted(translate("messages.status.welcome"))); """.formatted(translate("messages.status.welcome")));
else { else {
final var filePath = file.getPath(); final var filePath = file.getPath();
final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1)); final HttpUtils.MediaType fromExtension = HttpUtils.MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));
response.type((fromExtension != null) ? fromExtension.getMIME() : "text/plain") ctx.contentType((fromExtension != null) ? fromExtension.getMIME() : "text/plain").result(FileUtils.read(filePath));
.send(FileUtils.read(filePath));
} }
}); });
} }
...@@ -171,8 +169,8 @@ public final class HttpServer { ...@@ -171,8 +169,8 @@ public final class HttpServer {
* Handles unhandled endpoints on the Express application. * Handles unhandled endpoints on the Express application.
*/ */
public static class UnhandledRequestRouter implements Router { public static class UnhandledRequestRouter implements Router {
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
handle.error(404, context -> { javalin.error(404, context -> {
if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING) if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING)
Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url())); Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url()));
context.contentType("text/html"); context.contentType("text/html");
...@@ -193,7 +191,7 @@ public final class HttpServer { ...@@ -193,7 +191,7 @@ public final class HttpServer {
"""); """);
else { else {
final var filePath = file.getPath(); final var filePath = file.getPath();
final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1)); final HttpUtils.MediaType fromExtension = HttpUtils.MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));
context.contentType((fromExtension != null) ? fromExtension.getMIME() : "text/plain") context.contentType((fromExtension != null) ? fromExtension.getMIME() : "text/plain")
.result(FileUtils.read(filePath)); .result(FileUtils.read(filePath));
} }
......
package emu.grasscutter.server.http; package emu.grasscutter.server.http;
import express.Express;
import io.javalin.Javalin; import io.javalin.Javalin;
/** /**
* Defines routes for an {@link Express} instance. * Defines routes for an {@link Javalin} instance.
*/ */
public interface Router { public interface Router {
/** /**
* Called when the router is initialized by Express. * Called when the router is initialized by Express.
* @param express An Express instance. * @param javalin A Javalin instance.
*/ */
void applyRoutes(Express express, Javalin handle); void applyRoutes(Javalin javalin);
} }
...@@ -2,16 +2,13 @@ package emu.grasscutter.server.http.dispatch; ...@@ -2,16 +2,13 @@ package emu.grasscutter.server.http.dispatch;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem; import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.auth.OAuthAuthenticator;
import emu.grasscutter.auth.OAuthAuthenticator.ClientType; import emu.grasscutter.auth.OAuthAuthenticator.ClientType;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.*; import emu.grasscutter.server.http.objects.*;
import emu.grasscutter.server.http.objects.ComboTokenReqJson.LoginTokenData; import emu.grasscutter.server.http.objects.ComboTokenReqJson.LoginTokenData;
import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.JsonUtils;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
...@@ -19,40 +16,40 @@ import static emu.grasscutter.utils.Language.translate; ...@@ -19,40 +16,40 @@ import static emu.grasscutter.utils.Language.translate;
* Handles requests related to authentication. (aka dispatch) * Handles requests related to authentication. (aka dispatch)
*/ */
public final class DispatchHandler implements Router { public final class DispatchHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
// Username & Password login (from client). // Username & Password login (from client).
express.post("/hk4e_global/mdk/shield/api/login", DispatchHandler::clientLogin); javalin.post("/hk4e_global/mdk/shield/api/login", DispatchHandler::clientLogin);
// Cached token login (from registry). // Cached token login (from registry).
express.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin); javalin.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin);
// Combo token login (from session key). // Combo token login (from session key).
express.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin); javalin.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin);
// External login (from other clients). // External login (from other clients).
express.get("/authentication/type", (request, response) -> response.send(Grasscutter.getAuthenticationSystem().getClass().getSimpleName())); javalin.get("/authentication/type", ctx -> ctx.result(Grasscutter.getAuthenticationSystem().getClass().getSimpleName()));
express.post("/authentication/login", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() javalin.post("/authentication/login", ctx -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(request, response))); .handleLogin(AuthenticationSystem.fromExternalRequest(ctx)));
express.post("/authentication/register", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() javalin.post("/authentication/register", ctx -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handleAccountCreation(AuthenticationSystem.fromExternalRequest(request, response))); .handleAccountCreation(AuthenticationSystem.fromExternalRequest(ctx)));
express.post("/authentication/change_password", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() javalin.post("/authentication/change_password", ctx -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handlePasswordReset(AuthenticationSystem.fromExternalRequest(request, response))); .handlePasswordReset(AuthenticationSystem.fromExternalRequest(ctx)));
// External login (from OAuth2). // External login (from OAuth2).
express.post("/hk4e_global/mdk/shield/api/loginByThirdparty", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator() javalin.post("/hk4e_global/mdk/shield/api/loginByThirdparty", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(request, response))); .handleLogin(AuthenticationSystem.fromExternalRequest(ctx)));
express.get("/authentication/openid/redirect", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator() javalin.get("/authentication/openid/redirect", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleTokenProcess(AuthenticationSystem.fromExternalRequest(request, response))); .handleTokenProcess(AuthenticationSystem.fromExternalRequest(ctx)));
express.get("/Api/twitter_login", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator() javalin.get("/Api/twitter_login", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleRedirection(AuthenticationSystem.fromExternalRequest(request, response), ClientType.DESKTOP)); .handleRedirection(AuthenticationSystem.fromExternalRequest(ctx), ClientType.DESKTOP));
express.get("/sdkTwitterLogin.html", (request, response) -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator() javalin.get("/sdkTwitterLogin.html", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleRedirection(AuthenticationSystem.fromExternalRequest(request, response), ClientType.MOBILE)); .handleRedirection(AuthenticationSystem.fromExternalRequest(ctx), ClientType.MOBILE));
} }
/** /**
* @route /hk4e_global/mdk/shield/api/login * @route /hk4e_global/mdk/shield/api/login
*/ */
private static void clientLogin(Request request, Response response) { private static void clientLogin(Context ctx) {
// Parse body data. // Parse body data.
String rawBodyData = request.ctx().body(); String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, LoginAccountRequestJson.class); var bodyData = JsonUtils.decode(rawBodyData, LoginAccountRequestJson.class);
// Validate body data. // Validate body data.
...@@ -62,20 +59,20 @@ public final class DispatchHandler implements Router { ...@@ -62,20 +59,20 @@ public final class DispatchHandler implements Router {
// Pass data to authentication handler. // Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem() var responseData = Grasscutter.getAuthenticationSystem()
.getPasswordAuthenticator() .getPasswordAuthenticator()
.authenticate(AuthenticationSystem.fromPasswordRequest(request, bodyData)); .authenticate(AuthenticationSystem.fromPasswordRequest(ctx, bodyData));
// Send response. // Send response.
response.send(responseData); ctx.json(responseData);
// Log to console. // Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", ctx.ip()));
} }
/** /**
* @route /hk4e_global/mdk/shield/api/verify * @route /hk4e_global/mdk/shield/api/verify
*/ */
private static void tokenLogin(Request request, Response response) { private static void tokenLogin(Context ctx) {
// Parse body data. // Parse body data.
String rawBodyData = request.ctx().body(); String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, LoginTokenRequestJson.class); var bodyData = JsonUtils.decode(rawBodyData, LoginTokenRequestJson.class);
// Validate body data. // Validate body data.
...@@ -85,20 +82,20 @@ public final class DispatchHandler implements Router { ...@@ -85,20 +82,20 @@ public final class DispatchHandler implements Router {
// Pass data to authentication handler. // Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem() var responseData = Grasscutter.getAuthenticationSystem()
.getTokenAuthenticator() .getTokenAuthenticator()
.authenticate(AuthenticationSystem.fromTokenRequest(request, bodyData)); .authenticate(AuthenticationSystem.fromTokenRequest(ctx, bodyData));
// Send response. // Send response.
response.send(responseData); ctx.json(responseData);
// Log to console. // Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", ctx.ip()));
} }
/** /**
* @route /hk4e_global/combo/granter/login/v2/login * @route /hk4e_global/combo/granter/login/v2/login
*/ */
private static void sessionKeyLogin(Request request, Response response) { private static void sessionKeyLogin(Context ctx) {
// Parse body data. // Parse body data.
String rawBodyData = request.ctx().body(); String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, ComboTokenReqJson.class); var bodyData = JsonUtils.decode(rawBodyData, ComboTokenReqJson.class);
// Validate body data. // Validate body data.
...@@ -111,11 +108,11 @@ public final class DispatchHandler implements Router { ...@@ -111,11 +108,11 @@ public final class DispatchHandler implements Router {
// Pass data to authentication handler. // Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem() var responseData = Grasscutter.getAuthenticationSystem()
.getSessionKeyAuthenticator() .getSessionKeyAuthenticator()
.authenticate(AuthenticationSystem.fromComboTokenRequest(request, bodyData, tokenData)); .authenticate(AuthenticationSystem.fromComboTokenRequest(ctx, bodyData, tokenData));
// Send response. // Send response.
response.send(responseData); ctx.json(responseData);
// Log to console. // Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", ctx.ip()));
} }
} }
...@@ -11,20 +11,12 @@ import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; ...@@ -11,20 +11,12 @@ import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.QueryCurRegionRspJson; import emu.grasscutter.server.http.objects.QueryCurRegionRspJson;
import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.security.Signature; import java.security.Signature;
...@@ -107,36 +99,36 @@ public final class RegionHandler implements Router { ...@@ -107,36 +99,36 @@ public final class RegionHandler implements Router {
regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray()); regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray());
} }
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
express.get("/query_region_list", RegionHandler::queryRegionList); javalin.get("/query_region_list", RegionHandler::queryRegionList);
express.get("/query_cur_region/:region", RegionHandler::queryCurrentRegion ); javalin.get("/query_cur_region/{region}", RegionHandler::queryCurrentRegion );
} }
/** /**
* @route /query_region_list * @route /query_region_list
*/ */
private static void queryRegionList(Request request, Response response) { private static void queryRegionList(Context ctx) {
// Invoke event. // Invoke event.
QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); event.call(); QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); event.call();
// Respond with event result. // Respond with event result.
response.send(event.getRegionList()); ctx.result(event.getRegionList());
// Log to console. // Log to console.
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", request.ip())); Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", ctx.ip()));
} }
/** /**
* @route /query_cur_region/:region * @route /query_cur_region/{region}
*/ */
private static void queryCurrentRegion(Request request, Response response) { private static void queryCurrentRegion(Context ctx) {
// Get region to query. // Get region to query.
String regionName = request.params("region"); String regionName = ctx.pathParam("region");
String versionName = request.query("version"); String versionName = ctx.queryParam("version");
var region = regions.get(regionName); var region = regions.get(regionName);
// Get region data. // Get region data.
String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw==";
if (request.query().values().size() > 0) { if (ctx.queryParamMap().values().size() > 0) {
if (region != null) if (region != null)
regionData = region.getBase64(); regionData = region.getBase64();
} }
...@@ -150,18 +142,18 @@ public final class RegionHandler implements Router { ...@@ -150,18 +142,18 @@ public final class RegionHandler implements Router {
try { try {
QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call();
if (request.query("dispatchSeed") == null) { if (ctx.queryParam("dispatchSeed") == null) {
// More love for UA Patch players // More love for UA Patch players
var rsp = new QueryCurRegionRspJson(); var rsp = new QueryCurRegionRspJson();
rsp.content = event.getRegionInfo(); rsp.content = event.getRegionInfo();
rsp.sign = "TW9yZSBsb3ZlIGZvciBVQSBQYXRjaCBwbGF5ZXJz"; rsp.sign = "TW9yZSBsb3ZlIGZvciBVQSBQYXRjaCBwbGF5ZXJz";
response.send(rsp); ctx.json(rsp);
return; return;
} }
String key_id = request.query("key_id"); String key_id = ctx.queryParam("key_id");
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, key_id.equals("3") ? Crypto.CUR_OS_ENCRYPT_KEY : Crypto.CUR_CN_ENCRYPT_KEY); cipher.init(Cipher.ENCRYPT_MODE, key_id.equals("3") ? Crypto.CUR_OS_ENCRYPT_KEY : Crypto.CUR_CN_ENCRYPT_KEY);
var regionInfo = Utils.base64Decode(event.getRegionInfo()); var regionInfo = Utils.base64Decode(event.getRegionInfo());
...@@ -189,7 +181,7 @@ public final class RegionHandler implements Router { ...@@ -189,7 +181,7 @@ public final class RegionHandler implements Router {
rsp.content = Utils.base64Encode(encryptedRegionInfoStream.toByteArray()); rsp.content = Utils.base64Encode(encryptedRegionInfoStream.toByteArray());
rsp.sign = Utils.base64Encode(privateSignature.sign()); rsp.sign = Utils.base64Encode(privateSignature.sign());
response.send(rsp); ctx.json(rsp);
} }
catch (Exception e) { catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e); Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e);
...@@ -199,10 +191,10 @@ public final class RegionHandler implements Router { ...@@ -199,10 +191,10 @@ public final class RegionHandler implements Router {
// Invoke event. // Invoke event.
QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call();
// Respond with event result. // Respond with event result.
response.send(event.getRegionInfo()); ctx.result(event.getRegionInfo());
} }
// Log to console. // Log to console.
Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region/%s", request.ip(), regionName)); Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region/%s", ctx.ip(), regionName));
} }
/** /**
......
package emu.grasscutter.server.http.documentation; package emu.grasscutter.server.http.documentation;
import express.http.Request; import io.javalin.http.Context;
import express.http.Response;
interface DocumentationHandler { interface DocumentationHandler {
void handle(Request request, Response response); void handle(Context ctx);
} }
package emu.grasscutter.server.http.documentation; package emu.grasscutter.server.http.documentation;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import express.Express;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context;
public final class DocumentationServerHandler implements Router { public final class DocumentationServerHandler implements Router {
@Override @Override
public void applyRoutes(Express express, Javalin handle) { public void applyRoutes(Javalin javalin) {
final RootRequestHandler root = new RootRequestHandler(); final RootRequestHandler root = new RootRequestHandler();
final HandbookRequestHandler handbook = new HandbookRequestHandler(); final HandbookRequestHandler handbook = new HandbookRequestHandler();
final GachaMappingRequestHandler gachaMapping = new GachaMappingRequestHandler(); final GachaMappingRequestHandler gachaMapping = new GachaMappingRequestHandler();
express.get("/documentation/handbook", handbook::handle); // TODO: Removal
express.get("/documentation/gachamapping", gachaMapping::handle); // TODO: Forward /documentation requests to https://grasscutter.io/wiki
express.get("/documentation", root::handle); javalin.get("/documentation/handbook", handbook::handle);
javalin.get("/documentation/gachamapping", gachaMapping::handle);
javalin.get("/documentation", root::handle);
} }
} }
package emu.grasscutter.server.http.documentation; package emu.grasscutter.server.http.documentation;
import emu.grasscutter.tools.Tools; import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.HttpUtils;
import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Language;
import express.http.Request; import io.javalin.http.Context;
import express.http.Response;
import static emu.grasscutter.config.Configuration.DOCUMENT_LANGUAGE; import static emu.grasscutter.config.Configuration.DOCUMENT_LANGUAGE;
...@@ -17,10 +17,8 @@ final class GachaMappingRequestHandler implements DocumentationHandler { ...@@ -17,10 +17,8 @@ final class GachaMappingRequestHandler implements DocumentationHandler {
} }
@Override @Override
public void handle(Request request, Response response) { public void handle(Context ctx) {
final int langIdx = Language.TextStrings.MAP_LANGUAGES.getOrDefault(DOCUMENT_LANGUAGE, 0); // TODO: This should really be based off the client language somehow final int langIdx = Language.TextStrings.MAP_LANGUAGES.getOrDefault(DOCUMENT_LANGUAGE, 0); // TODO: This should really be based off the client language somehow
response.set("Content-Type", "application/json") ctx.contentType(HttpUtils.MediaType._json.getMIME()).result(gachaJsons.get(langIdx));
.ctx()
.result(gachaJsons.get(langIdx));
} }
} }
...@@ -10,10 +10,10 @@ import emu.grasscutter.data.excels.ItemData; ...@@ -10,10 +10,10 @@ import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.MonsterData; import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.SceneData; import emu.grasscutter.data.excels.SceneData;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.HttpUtils;
import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Language;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import express.http.Request; import io.javalin.http.Context;
import express.http.Response;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
...@@ -36,12 +36,13 @@ final class HandbookRequestHandler implements DocumentationHandler { ...@@ -36,12 +36,13 @@ final class HandbookRequestHandler implements DocumentationHandler {
} }
@Override @Override
public void handle(Request request, Response response) { public void handle(Context ctx) {
final int langIdx = Language.TextStrings.MAP_LANGUAGES.getOrDefault(DOCUMENT_LANGUAGE, 0); // TODO: This should really be based off the client language somehow final int langIdx = Language.TextStrings.MAP_LANGUAGES.getOrDefault(DOCUMENT_LANGUAGE, 0); // TODO: This should really be based off the client language somehow
if (template == null) { if (template == null) {
response.status(500); ctx.status(500);
} else { } else {
response.send(handbookHtmls.get(langIdx)); ctx.contentType(HttpUtils.MediaType._html.getMIME());
ctx.result(handbookHtmls.get(langIdx));
} }
} }
......
...@@ -4,11 +4,11 @@ import static emu.grasscutter.config.Configuration.DATA; ...@@ -4,11 +4,11 @@ import static emu.grasscutter.config.Configuration.DATA;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.HttpUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import express.http.Request; import io.javalin.http.Context;
import express.http.Response;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
...@@ -27,15 +27,16 @@ final class RootRequestHandler implements DocumentationHandler { ...@@ -27,15 +27,16 @@ final class RootRequestHandler implements DocumentationHandler {
} }
@Override @Override
public void handle(Request request, Response response) { public void handle(Context ctx) {
if (template == null) { if (template == null) {
response.status(500); ctx.status(500);
return; return;
} }
String content = template.replace("{{TITLE}}", translate("documentation.index.title")) String content = template.replace("{{TITLE}}", translate("documentation.index.title"))
.replace("{{ITEM_HANDBOOK}}", translate("documentation.index.handbook")) .replace("{{ITEM_HANDBOOK}}", translate("documentation.index.handbook"))
.replace("{{ITEM_GACHA_MAPPING}}", translate("documentation.index.gacha_mapping")); .replace("{{ITEM_GACHA_MAPPING}}", translate("documentation.index.gacha_mapping"));
response.send(content); ctx.contentType(HttpUtils.MediaType._html.getMIME());
ctx.result(content);
} }
} }
...@@ -5,44 +5,39 @@ import emu.grasscutter.data.DataLoader; ...@@ -5,44 +5,39 @@ import emu.grasscutter.data.DataLoader;
import emu.grasscutter.server.http.objects.HttpJsonResponse; import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.HttpUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.MediaType;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context;
import static emu.grasscutter.config.Configuration.*; import static emu.grasscutter.config.Configuration.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
/** /**
* Handles requests related to the announcements page. * Handles requests related to the announcements page.
*/ */
public final class AnnouncementsHandler implements Router { public final class AnnouncementsHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
// hk4e-api-os.hoyoverse.com // hk4e-api-os.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); HttpUtils.allRoutes(javalin, "/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}"));
// hk4e-api-os.hoyoverse.com // hk4e-api-os.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAlertAnn", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); HttpUtils.allRoutes(javalin,"/common/hk4e_global/announcement/api/getAlertAnn", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}"));
// hk4e-api-os.hoyoverse.com // hk4e-api-os.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement); HttpUtils.allRoutes(javalin,"/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement);
// hk4e-api-os-static.hoyoverse.com // hk4e-api-os-static.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement); HttpUtils.allRoutes(javalin,"/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement);
// hk4e-sdk-os.hoyoverse.com // hk4e-sdk-os.hoyoverse.com
express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); HttpUtils.allRoutes(javalin,"/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}"));
express.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources); javalin.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources);
} }
private static void getAnnouncement(Request request, Response response) { private static void getAnnouncement(Context ctx) {
String data = ""; String data = "";
if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { if (Objects.equals(ctx.endpointHandlerPath(), "/common/hk4e_global/announcement/api/getAnnContent")) {
try { try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncement.json")); data = FileUtils.readToString(DataLoader.load("GameAnnouncement.json"));
} catch (Exception e) { } catch (Exception e) {
...@@ -50,7 +45,7 @@ public final class AnnouncementsHandler implements Router { ...@@ -50,7 +45,7 @@ public final class AnnouncementsHandler implements Router {
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e); Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
} }
} }
} else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { } else if (Objects.equals(ctx.endpointHandlerPath(), "/common/hk4e_global/announcement/api/getAnnList")) {
try { try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncementList.json")); data = FileUtils.readToString(DataLoader.load("GameAnnouncementList.json"));
} catch (Exception e) { } catch (Exception e) {
...@@ -59,11 +54,11 @@ public final class AnnouncementsHandler implements Router { ...@@ -59,11 +54,11 @@ public final class AnnouncementsHandler implements Router {
} }
} }
} else { } else {
response.send("{\"retcode\":404,\"message\":\"Unknown request path\"}"); ctx.result("{\"retcode\":404,\"message\":\"Unknown request path\"}");
} }
if (data.isEmpty()) { if (data.isEmpty()) {
response.send("{\"retcode\":500,\"message\":\"Unable to fetch requsted content\"}"); ctx.result("{\"retcode\":500,\"message\":\"Unable to fetch requsted content\"}");
return; return;
} }
...@@ -74,19 +69,19 @@ public final class AnnouncementsHandler implements Router { ...@@ -74,19 +69,19 @@ public final class AnnouncementsHandler implements Router {
data = data data = data
.replace("{{DISPATCH_PUBLIC}}", dispatchDomain) .replace("{{DISPATCH_PUBLIC}}", dispatchDomain)
.replace("{{SYSTEM_TIME}}", String.valueOf(System.currentTimeMillis())); .replace("{{SYSTEM_TIME}}", String.valueOf(System.currentTimeMillis()));
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}"); ctx.result("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}");
} }
private static void getPageResources(Request request, Response response) { private static void getPageResources(Context ctx) {
try (InputStream filestream = DataLoader.load(request.path())) { try (InputStream filestream = DataLoader.load(ctx.path())) {
String possibleFilename = Utils.toFilePath(DATA(request.path())); String possibleFilename = Utils.toFilePath(DATA(ctx.path()));
MediaType fromExtension = MediaType.getByExtension(possibleFilename.substring(possibleFilename.lastIndexOf(".") + 1)); HttpUtils.MediaType fromExtension = HttpUtils.MediaType.getByExtension(possibleFilename.substring(possibleFilename.lastIndexOf(".") + 1));
response.type((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream"); ctx.contentType((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream");
response.send(filestream.readAllBytes()); ctx.result(filestream.readAllBytes());
} catch (Exception e) { } catch (Exception e) {
Grasscutter.getLogger().warn("File does not exist: " + request.path()); Grasscutter.getLogger().warn("File does not exist: " + ctx.path());
response.status(404); ctx.status(404);
} }
} }
} }
...@@ -7,13 +7,11 @@ import emu.grasscutter.game.gacha.GachaBanner; ...@@ -7,13 +7,11 @@ import emu.grasscutter.game.gacha.GachaBanner;
import emu.grasscutter.game.gacha.GachaSystem; import emu.grasscutter.game.gacha.GachaSystem;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.HttpUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context;
import io.javalin.http.staticfiles.Location; import io.javalin.http.staticfiles.Location;
import java.io.File; import java.io.File;
...@@ -31,38 +29,38 @@ import static emu.grasscutter.utils.Language.translate; ...@@ -31,38 +29,38 @@ import static emu.grasscutter.utils.Language.translate;
public final class GachaHandler implements Router { public final class GachaHandler implements Router {
public static final String gachaMappings = DATA(Utils.toFilePath("gacha/mappings.js")); public static final String gachaMappings = DATA(Utils.toFilePath("gacha/mappings.js"));
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
express.get("/gacha", GachaHandler::gachaRecords); javalin.get("/gacha", GachaHandler::gachaRecords);
express.get("/gacha/details", GachaHandler::gachaDetails); javalin.get("/gacha/details", GachaHandler::gachaDetails);
express.useStaticFallback("/gacha/mappings", gachaMappings, Location.EXTERNAL); javalin._conf.addSinglePageRoot("/gacha/mappings", gachaMappings, Location.EXTERNAL);
} }
private static void gachaRecords(Request request, Response response) { private static void gachaRecords(Context ctx) {
File recordsTemplate = new File(Utils.toFilePath(DATA("gacha/records.html"))); File recordsTemplate = new File(Utils.toFilePath(DATA("gacha/records.html")));
if (!recordsTemplate.exists()) { if (!recordsTemplate.exists()) {
Grasscutter.getLogger().warn("File does not exist: " + recordsTemplate); Grasscutter.getLogger().warn("File does not exist: " + recordsTemplate);
response.status(500); ctx.status(500);
return; return;
} }
String sessionKey = request.query("s"); String sessionKey = ctx.queryParam("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if (account == null) { if (account == null) {
response.status(403).send("Requested account was not found"); ctx.status(403).result("Requested account was not found");
return; return;
} }
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId()); Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
if (player == null) { if (player == null) {
response.status(403).send("No player associated with requested account"); ctx.status(403).result("No player associated with requested account");
return; return;
} }
int page = 0, gachaType = 0; int page = 0, gachaType = 0;
if (request.query("p") != null) if (ctx.queryParam("p") != null)
page = Integer.parseInt(request.query("p")); page = Integer.parseInt(ctx.queryParam("p"));
if (request.query("gachaType") != null) if (ctx.queryParam("gachaType") != null)
gachaType = Integer.parseInt(request.query("gachaType")); gachaType = Integer.parseInt(ctx.queryParam("gachaType"));
String records = DatabaseHelper.getGachaRecords(player.getUid(), page, gachaType).toString(); String records = DatabaseHelper.getGachaRecords(player.getUid(), page, gachaType).toString();
long maxPage = DatabaseHelper.getGachaRecordsMaxPage(player.getUid(), page, gachaType); long maxPage = DatabaseHelper.getGachaRecordsMaxPage(player.getUid(), page, gachaType);
...@@ -74,26 +72,27 @@ public final class GachaHandler implements Router { ...@@ -74,26 +72,27 @@ public final class GachaHandler implements Router {
.replace("{{DATE}}", translate(player, "gacha.records.date")) .replace("{{DATE}}", translate(player, "gacha.records.date"))
.replace("{{ITEM}}", translate(player, "gacha.records.item")) .replace("{{ITEM}}", translate(player, "gacha.records.item"))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale())); .replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
response.send(template); ctx.contentType(HttpUtils.MediaType._html.getMIME());
ctx.result(template);
} }
private static void gachaDetails(Request request, Response response) { private static void gachaDetails(Context ctx) {
File detailsTemplate = new File(Utils.toFilePath(DATA("gacha/details.html"))); File detailsTemplate = new File(Utils.toFilePath(DATA("gacha/details.html")));
if (!detailsTemplate.exists()) { if (!detailsTemplate.exists()) {
Grasscutter.getLogger().warn("File does not exist: " + detailsTemplate); Grasscutter.getLogger().warn("File does not exist: " + detailsTemplate);
response.status(500); ctx.status(500);
return; return;
} }
String sessionKey = request.query("s"); String sessionKey = ctx.queryParam("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if (account == null) { if (account == null) {
response.status(403).send("Requested account was not found"); ctx.status(403).result("Requested account was not found");
return; return;
} }
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId()); Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
if (player == null) { if (player == null) {
response.status(403).send("No player associated with requested account"); ctx.status(403).result("No player associated with requested account");
return; return;
} }
...@@ -107,7 +106,7 @@ public final class GachaHandler implements Router { ...@@ -107,7 +106,7 @@ public final class GachaHandler implements Router {
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale())); .replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
// Get the banner info for the banner we want. // Get the banner info for the banner we want.
int scheduleId = Integer.parseInt(request.query("scheduleId")); int scheduleId = Integer.parseInt(ctx.queryParam("scheduleId"));
GachaSystem manager = Grasscutter.getGameServer().getGachaSystem(); GachaSystem manager = Grasscutter.getGameServer().getGachaSystem();
GachaBanner banner = manager.getGachaBanners().get(scheduleId); GachaBanner banner = manager.getGachaBanners().get(scheduleId);
...@@ -135,6 +134,7 @@ public final class GachaHandler implements Router { ...@@ -135,6 +134,7 @@ public final class GachaHandler implements Router {
template = template.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]"); template = template.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
// Done. // Done.
response.send(template); ctx.contentType(HttpUtils.MediaType._html.getMIME());
ctx.result(template);
} }
} }
...@@ -7,53 +7,52 @@ import emu.grasscutter.Grasscutter; ...@@ -7,53 +7,52 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.objects.HttpJsonResponse; import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.WebStaticVersionResponse; import emu.grasscutter.server.http.objects.WebStaticVersionResponse;
import express.Express; import emu.grasscutter.utils.HttpUtils;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context;
/** /**
* Handles all generic, hard-coded responses. * Handles all generic, hard-coded responses.
*/ */
public final class GenericHandler implements Router { public final class GenericHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
// hk4e-sdk-os.hoyoverse.com // hk4e-sdk-os.hoyoverse.com
express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); javalin.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}"));
// hk4e-sdk-os.hoyoverse.com // hk4e-sdk-os.hoyoverse.com
// this could be either GET or POST based on the observation of different clients // this could be either GET or POST based on the observation of different clients
express.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}")); HttpUtils.allRoutes(javalin, "/hk4e_global/combo/granter/api/compareProtocolVersion", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}"));
// api-account-os.hoyoverse.com // api-account-os.hoyoverse.com
express.post("/account/risky/api/check", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); javalin.post("/account/risky/api/check", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}"));
// sdk-os-static.hoyoverse.com // sdk-os-static.hoyoverse.com
express.get("/combo/box/api/config/sdk/combo", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}")); javalin.get("/combo/box/api/config/sdk/combo", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}"));
// hk4e-sdk-os-static.hoyoverse.com // hk4e-sdk-os-static.hoyoverse.com
express.get("/hk4e_global/combo/granter/api/getConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}")); javalin.get("/hk4e_global/combo/granter/api/getConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}"));
// hk4e-sdk-os-static.hoyoverse.com // hk4e-sdk-os-static.hoyoverse.com
express.get("/hk4e_global/mdk/shield/api/loadConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}")); javalin.get("/hk4e_global/mdk/shield/api/loadConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}"));
// Test api? // Test api?
// abtest-api-data-sg.hoyoverse.com // abtest-api-data-sg.hoyoverse.com
express.post("/data_abtest_api/config/experiment/list", new HttpJsonResponse("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}")); javalin.post("/data_abtest_api/config/experiment/list", new HttpJsonResponse("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}"));
// log-upload-os.mihoyo.com // log-upload-os.mihoyo.com
express.all("/log/sdk/upload", new HttpJsonResponse("{\"code\":0}")); HttpUtils.allRoutes(javalin, "/log/sdk/upload", new HttpJsonResponse("{\"code\":0}"));
express.all("/sdk/upload", new HttpJsonResponse("{\"code\":0}")); HttpUtils.allRoutes(javalin, "/sdk/upload", new HttpJsonResponse("{\"code\":0}"));
express.post("/sdk/dataUpload", new HttpJsonResponse("{\"code\":0}")); javalin.post("/sdk/dataUpload", new HttpJsonResponse("{\"code\":0}"));
// /perf/config/verify?device_id=xxx&platform=x&name=xxx // /perf/config/verify?device_id=xxx&platform=x&name=xxx
express.all("/perf/config/verify", new HttpJsonResponse("{\"code\":0}")); HttpUtils.allRoutes(javalin, "/perf/config/verify", new HttpJsonResponse("{\"code\":0}"));
// webstatic-sea.hoyoverse.com // webstatic-sea.hoyoverse.com
express.get("/admin/mi18n/plat_oversea/*", new WebStaticVersionResponse()); javalin.get("/admin/mi18n/plat_oversea/*", new WebStaticVersionResponse());
express.get("/status/server", GenericHandler::serverStatus); javalin.get("/status/server", GenericHandler::serverStatus);
} }
private static void serverStatus(Request request, Response response) { private static void serverStatus(Context ctx) {
int playerCount = Grasscutter.getGameServer().getPlayers().size(); int playerCount = Grasscutter.getGameServer().getPlayers().size();
int maxPlayer = ACCOUNT.maxPlayer; int maxPlayer = ACCOUNT.maxPlayer;
String version = GameConstants.VERSION; String version = GameConstants.VERSION;
response.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"maxPlayer\":" + maxPlayer + ",\"version\":\"" + version + "\"}}"); ctx.result("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"maxPlayer\":" + maxPlayer + ",\"version\":\"" + version + "\"}}");
} }
} }
package emu.grasscutter.server.http.handlers; package emu.grasscutter.server.http.handlers;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context;
/** /**
* Handles logging requests made to the server. * Handles logging requests made to the server.
*/ */
public final class LogHandler implements Router { public final class LogHandler implements Router {
@Override public void applyRoutes(Express express, Javalin handle) { @Override public void applyRoutes(Javalin javalin) {
// overseauspider.yuanshen.com // overseauspider.yuanshen.com
express.post("/log", LogHandler::log); javalin.post("/log", LogHandler::log);
// log-upload-os.mihoyo.com // log-upload-os.mihoyo.com
express.post("/crash/dataUpload", LogHandler::log); javalin.post("/crash/dataUpload", LogHandler::log);
} }
private static void log(Request request, Response response) { private static void log(Context ctx) {
// TODO: Figure out how to dump request body and log to file. // TODO: Figure out how to dump request body and log to file.
response.send("{\"code\":0}"); ctx.result("{\"code\":0}");
} }
} }
...@@ -6,14 +6,14 @@ import java.util.Objects; ...@@ -6,14 +6,14 @@ import java.util.Objects;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerDebugMode;
import express.http.HttpContextHandler; import io.javalin.http.Context;
import express.http.Request; import io.javalin.http.Handler;
import express.http.Response; import org.jetbrains.annotations.NotNull;
import static emu.grasscutter.config.Configuration.*; import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
public final class HttpJsonResponse implements HttpContextHandler { public final class HttpJsonResponse implements Handler {
private final String response; private final String response;
private final String[] missingRoutes = { // TODO: When http requests for theses routes are found please remove it from this list and update the route request type in the DispatchServer private final String[] missingRoutes = { // TODO: When http requests for theses routes are found please remove it from this list and update the route request type in the DispatchServer
"/common/hk4e_global/announcement/api/getAlertPic", "/common/hk4e_global/announcement/api/getAlertPic",
...@@ -33,11 +33,11 @@ public final class HttpJsonResponse implements HttpContextHandler { ...@@ -33,11 +33,11 @@ public final class HttpJsonResponse implements HttpContextHandler {
} }
@Override @Override
public void handle(Request req, Response res) throws IOException { public void handle(@NotNull Context ctx) throws Exception {
// Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled // Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled
if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> Objects.equals(x, req.baseUrl()))) { if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> Objects.equals(x, ctx.endpointHandlerPath()))) {
Grasscutter.getLogger().info(translate("messages.dispatch.request", req.ip(), req.method(), req.baseUrl()) + (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING ? "(MISSING)" : "")); Grasscutter.getLogger().info(translate("messages.dispatch.request", ctx.ip(), ctx.method(), ctx.endpointHandlerPath()) + (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING ? "(MISSING)" : ""));
} }
res.send(response); ctx.result(response);
} }
} }
package emu.grasscutter.server.http.objects; package emu.grasscutter.server.http.objects;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.HttpUtils;
import express.http.HttpContextHandler; import io.javalin.http.Context;
import express.http.MediaType; import io.javalin.http.Handler;
import express.http.Request;
import express.http.Response;
import io.javalin.core.util.FileUtil;
import static emu.grasscutter.config.Configuration.DATA;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO; import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
public class WebStaticVersionResponse implements HttpContextHandler { public class WebStaticVersionResponse implements Handler {
@Override @Override
public void handle(Request request, Response response) throws IOException { public void handle(Context ctx) throws IOException {
String requestFor = request.path().substring(request.path().lastIndexOf("-") + 1); String requestFor = ctx.path().substring(ctx.path().lastIndexOf("-") + 1);
getPageResources("/webstatic/" + requestFor, response); getPageResources("/webstatic/" + requestFor, ctx);
return; return;
} }
private static void getPageResources(String path, Response response) { private static void getPageResources(String path, Context ctx) {
try (InputStream filestream = FileUtils.readResourceAsStream(path)) { try (InputStream filestream = FileUtils.readResourceAsStream(path)) {
MediaType fromExtension = MediaType.getByExtension(path.substring(path.lastIndexOf(".") + 1)); HttpUtils.MediaType fromExtension = HttpUtils.MediaType.getByExtension(path.substring(path.lastIndexOf(".") + 1));
response.type((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream"); ctx.contentType((fromExtension != null) ? fromExtension.getMIME() : "application/octet-stream");
response.send(filestream.readAllBytes()); ctx.result(filestream.readAllBytes());
} catch (Exception e) { } catch (Exception e) {
if (DISPATCH_INFO.logRequests == Grasscutter.ServerDebugMode.MISSING) { if (DISPATCH_INFO.logRequests == Grasscutter.ServerDebugMode.MISSING) {
Grasscutter.getLogger().warn("Webstatic File Missing: " + path); Grasscutter.getLogger().warn("Webstatic File Missing: " + path);
} }
response.status(404); ctx.status(404);
} }
} }
} }
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment