From 03a287623375d619c8c1fb53e4c64132c7fe7cd8 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Wed, 11 May 2022 11:46:36 -0400 Subject: [PATCH 01/12] Add plugin loggers --- src/main/java/emu/grasscutter/plugin/Plugin.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/emu/grasscutter/plugin/Plugin.java b/src/main/java/emu/grasscutter/plugin/Plugin.java index f1ce18a6b..b45e642a5 100644 --- a/src/main/java/emu/grasscutter/plugin/Plugin.java +++ b/src/main/java/emu/grasscutter/plugin/Plugin.java @@ -3,6 +3,8 @@ package emu.grasscutter.plugin; import emu.grasscutter.Grasscutter; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.server.game.GameServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.InputStream; @@ -19,6 +21,7 @@ public abstract class Plugin { private PluginIdentifier identifier; private URLClassLoader classLoader; private File dataFolder; + private Logger logger; /** * This method is reflected into. @@ -35,6 +38,7 @@ public abstract class Plugin { this.identifier = identifier; this.classLoader = classLoader; this.dataFolder = new File(PLUGINS_FOLDER, identifier.name); + this.logger = LoggerFactory.getLogger(identifier.name); if(!this.dataFolder.exists() && !this.dataFolder.mkdirs()) { Grasscutter.getLogger().warn("Failed to create plugin data folder for " + this.identifier.name); @@ -103,6 +107,14 @@ public abstract class Plugin { public final ServerHook getHandle() { return this.server; } + + /** + * Returns the plugin's logger. + * @return A SLF4J logger. + */ + public final Logger getLogger() { + return this.logger; + } /* Called when the plugin is first loaded. */ public void onLoad() { } From e131c3c5ed61490f6006b82b631f21c18010f1a9 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:37:17 -0400 Subject: [PATCH 02/12] Add `lombok` --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 4434ed28e..2074ec553 100644 --- a/build.gradle +++ b/build.gradle @@ -86,6 +86,9 @@ dependencies { implementation group: 'org.luaj', name: 'luaj-jse', version: '3.0.1' protobuf files('proto/') + + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' } configurations.all { From 0b21a439004312a4f89d308ee145dc815d5bc8b9 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:37:49 -0400 Subject: [PATCH 03/12] Update project to `1.1.2-dev` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2074ec553..68f6d449a 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 group = 'xyz.grasscutters' -version = '1.1.1-dev' +version = '1.1.2-dev' sourceCompatibility = 17 targetCompatibility = 17 From 39f23a0c47e97eb11c7a431d337178b0ee713949 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:38:17 -0400 Subject: [PATCH 04/12] Add new authentication system --- .../auth/AuthenticationSystem.java | 101 +++++++++++ .../emu/grasscutter/auth/Authenticator.java | 17 ++ .../auth/DefaultAuthentication.java | 40 +++++ .../auth/DefaultAuthenticators.java | 161 ++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 src/main/java/emu/grasscutter/auth/AuthenticationSystem.java create mode 100644 src/main/java/emu/grasscutter/auth/Authenticator.java create mode 100644 src/main/java/emu/grasscutter/auth/DefaultAuthentication.java create mode 100644 src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java new file mode 100644 index 000000000..dae3402f2 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -0,0 +1,101 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.server.http.objects.*; +import express.http.Request; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import javax.annotation.Nullable; + +/** + * Defines an authenticator for the server. + * Can be changed by plugins. + */ +public interface AuthenticationSystem { + + /** + * Called when a user requests to make an account. + * @param username The provided username. + * @param password The provided password. (SHA-256'ed) + */ + void createAccount(String username, String password); + + /** + * Called when a user requests to reset their password. + * @param username The username of the account to reset. + */ + void resetPassword(String username); + + /** + * This is the authenticator used for password authentication. + * @return An authenticator. + */ + Authenticator getPasswordAuthenticator(); + + /** + * This is the authenticator used for token authentication. + * @return An authenticator. + */ + Authenticator getTokenAuthenticator(); + + /** + * This is the authenticator used for session authentication. + * @return An authenticator. + */ + Authenticator getSessionKeyAuthenticator(); + + /** + * A data container that holds relevant data for authenticating a client. + * Call {@link AuthenticationRequest#builder()} to create a builder. + */ + @Builder @AllArgsConstructor @Getter + class AuthenticationRequest { + private final Request request; + @Nullable private final LoginAccountRequestJson passwordRequest; + @Nullable private final LoginTokenRequestJson tokenRequest; + @Nullable private final ComboTokenReqJson sessionKeyRequest; + @Nullable private final ComboTokenReqJson.LoginTokenData sessionKeyData; + } + + /** + * Generates an authentication request from a {@link LoginAccountRequestJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromPasswordRequest(Request request, LoginAccountRequestJson jsonData) { + return AuthenticationRequest.builder() + .request(request) + .passwordRequest(jsonData) + .build(); + } + + /** + * Generates an authentication request from a {@link LoginTokenRequestJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromTokenRequest(Request request, LoginTokenRequestJson jsonData) { + return AuthenticationRequest.builder() + .request(request) + .tokenRequest(jsonData) + .build(); + } + + /** + * Generates an authentication request from a {@link ComboTokenReqJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromComboTokenRequest(Request request, ComboTokenReqJson jsonData, + ComboTokenReqJson.LoginTokenData tokenData) { + return AuthenticationRequest.builder() + .request(request) + .sessionKeyRequest(jsonData) + .sessionKeyData(tokenData) + .build(); + } +} diff --git a/src/main/java/emu/grasscutter/auth/Authenticator.java b/src/main/java/emu/grasscutter/auth/Authenticator.java new file mode 100644 index 000000000..a5d756d8c --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/Authenticator.java @@ -0,0 +1,17 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.server.http.objects.*; + +/** + * Handles username/password authentication from the client. + * @param The response object type. Should be {@link LoginResultJson} or {@link ComboTokenResJson} + */ +public interface Authenticator { + + /** + * Attempt to authenticate the client with the provided credentials. + * @param request The authentication request wrapped in a {@link AuthenticationSystem.AuthenticationRequest} object. + * @return The result of the login in an object. + */ + T authenticate(AuthenticationSystem.AuthenticationRequest request); +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java new file mode 100644 index 000000000..2864b80b5 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -0,0 +1,40 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.auth.DefaultAuthenticators.*; +import emu.grasscutter.server.http.objects.ComboTokenResJson; +import emu.grasscutter.server.http.objects.LoginResultJson; + +/** + * The default Grasscutter authentication implementation. + * Allows all users to access any account. + */ +public final class DefaultAuthentication implements AuthenticationSystem { + private final Authenticator passwordAuthenticator = new PasswordAuthenticator(); + private final Authenticator tokenAuthenticator = new TokenAuthenticator(); + private final Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); + + @Override + public void createAccount(String username, String password) { + // Unhandled. The default authenticator doesn't store passwords. + } + + @Override + public void resetPassword(String username) { + // Unhandled. The default authenticator doesn't store passwords. + } + + @Override + public Authenticator getPasswordAuthenticator() { + return this.passwordAuthenticator; + } + + @Override + public Authenticator getTokenAuthenticator() { + return this.tokenAuthenticator; + } + + @Override + public Authenticator getSessionKeyAuthenticator() { + return this.sessionKeyAuthenticator; + } +} diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java new file mode 100644 index 000000000..298d24493 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -0,0 +1,161 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.server.http.objects.*; + +import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.utils.Language.translate; + +/** + * A class containing default authenticators. + */ +public final class DefaultAuthenticators { + + /** + * Handles the authentication request from the username & password form. + */ + public static class PasswordAuthenticator implements Authenticator { + @Override public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + var requestData = request.getPasswordRequest(); + assert requestData != null; // This should never be null. + + boolean successfulLogin = false; + String address = request.getRequest().ip(); + String responseMessage = translate("messages.dispatch.account.username_error"); + + // Get account from database. + Account account = DatabaseHelper.getAccountByName(requestData.account); + + // Check if account exists. + if(account == null && ACCOUNT.autoCreate) { + // This account has been created AUTOMATICALLY. There will be no permissions added. + account = DatabaseHelper.createAccountWithId(requestData.account, 0); + + // Check if the account was created successfully. + if(account == null) { + responseMessage = translate("messages.dispatch.account.username_create_error"); + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", address)); + } else { + // Add default permissions. + for (var permission : ACCOUNT.defaultPermissions) + account.addPermission(permission); + + // Continue with login. + successfulLogin = true; + + // Log the creation. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid)); + } + } else if(account != null) + successfulLogin = true; + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.generateSessionKey(); + response.data.account.email = account.getEmail(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_success", address, account.getId())); + } else { + response.retcode = -201; + response.message = responseMessage; + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", address)); + } + + return response; + } + } + + /** + * Handles the authentication request from the game when using a registry token. + */ + public static class TokenAuthenticator implements Authenticator { + @Override public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + var requestData = request.getTokenRequest(); + assert requestData != null; + + boolean successfulLogin; + String address = request.getRequest().ip(); + + // Log the attempt. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", address)); + + // Get account from database. + Account account = DatabaseHelper.getAccountById(requestData.uid); + + // Check if account exists/token is valid. + successfulLogin = account != null && account.getSessionKey().equals(requestData.token); + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.getSessionKey(); + response.data.account.email = account.getEmail(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_success", address, requestData.uid)); + } else { + response.retcode = -201; + response.message = translate("messages.dispatch.account.account_cache_error"); + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", address)); + } + + return response; + } + } + + /** + * Handles the authentication request from the game when using a combo token/session key. + */ + public static class SessionKeyAuthenticator implements Authenticator { + @Override public ComboTokenResJson authenticate(AuthenticationRequest request) { + var response = new ComboTokenResJson(); + + var requestData = request.getSessionKeyRequest(); + var loginData = request.getSessionKeyData(); + assert requestData != null; assert loginData != null; + + boolean successfulLogin; + String address = request.getRequest().ip(); + + // Get account from database. + Account account = DatabaseHelper.getAccountById(loginData.uid); + + // Check if account exists/token is valid. + successfulLogin = account != null && account.getSessionKey().equals(loginData.token); + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.open_id = account.getId(); + response.data.combo_id = "157795300"; + response.data.combo_token = account.generateLoginToken(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", address)); + } else { + response.retcode = -201; + response.message = translate("messages.dispatch.account.session_key_error"); + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", address)); + } + + return response; + } + } +} From a0067b664eaf5d38342e757915dd4e87b4ce7bbe Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:38:54 -0400 Subject: [PATCH 05/12] Add JSON-related methods to `Utils.java` --- .../java/emu/grasscutter/utils/Utils.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 4af62bfb4..1fe026bd8 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -6,10 +6,7 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.*; import java.time.temporal.TemporalAdjusters; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.Locale; +import java.util.*; import emu.grasscutter.Grasscutter; import io.netty.buffer.ByteBuf; @@ -308,10 +305,42 @@ public final class Utils { } /** - * get language code from Locale + * Gets the language code from a given locale. + * @param locale A locale. + * @return A string in the format of 'XX-XX'. */ public static String getLanguageCode(Locale locale) { return String.format("%s-%s", locale.getLanguage(), locale.getCountry()); } + /** + * Base64 encodes a given byte array. + * @param toEncode An array of bytes. + * @return A base64 encoded string. + */ + public static String base64Encode(byte[] toEncode) { + return Base64.getEncoder().encodeToString(toEncode); + } + + /** + * Base64 decodes a given string. + * @param toDecode A base64 encoded string. + * @return An array of bytes. + */ + public static byte[] base64Decode(String toDecode) { + return Base64.getDecoder().decode(toDecode); + } + + /** + * Safely JSON decodes a given string. + * @param jsonData The JSON-encoded data. + * @return JSON decoded data, or null if an exception occurred. + */ + public static T jsonDecode(String jsonData, Class classType) { + try { + return Grasscutter.getGsonFactory().fromJson(jsonData, classType); + } catch (Exception ignored) { + return null; + } + } } From 840f4706b5d7d39b2d6319a25c32ee7c522c5fa2 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:39:40 -0400 Subject: [PATCH 06/12] Refactor dispatch (now called HTTP) server (pt. 1) --- .../java/emu/grasscutter/Configuration.java | 7 +- .../java/emu/grasscutter/Grasscutter.java | 150 +++++++++----- .../server/dispatch/DispatchServer.java | 20 +- .../grasscutter/server/http/HttpServer.java | 176 +++++++++++++++++ .../emu/grasscutter/server/http/Router.java | 16 ++ .../server/http/dispatch/DispatchHandler.java | 100 ++++++++++ .../server/http/dispatch/RegionHandler.java | 186 ++++++++++++++++++ .../http/handlers/AnnouncementsHandler.java | 58 ++++++ .../server/http/handlers/GenericHandler.java | 47 +++++ .../server/http/handlers/LogHandler.java | 18 ++ .../http/objects/ComboTokenReqJson.java | 15 ++ .../http/objects/ComboTokenResJson.java | 17 ++ .../http/objects/LoginAccountRequestJson.java | 7 + .../server/http/objects/LoginResultJson.java | 38 ++++ .../http/objects/LoginTokenRequestJson.java | 6 + .../grasscutter/utils/ConfigContainer.java | 31 ++- src/main/resources/languages/en-US.json | 3 +- 17 files changed, 828 insertions(+), 67 deletions(-) create mode 100644 src/main/java/emu/grasscutter/server/http/HttpServer.java create mode 100644 src/main/java/emu/grasscutter/server/http/Router.java create mode 100644 src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 7adc334c1..52bfa65aa 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -34,11 +34,12 @@ public final class Configuration extends ConfigContainer { public static final Database DATABASE = config.databaseInfo; public static final Account ACCOUNT = config.account; - public static final Dispatch DISPATCH_INFO = config.server.dispatch; + public static final HTTP HTTP_INFO = config.server.http; public static final Game GAME_INFO = config.server.game; + public static final Dispatch DISPATCH_INFO = config.server.dispatch; - public static final Encryption DISPATCH_ENCRYPTION = config.server.dispatch.encryption; - public static final Policies DISPATCH_POLICIES = config.server.dispatch.policies; + public static final Encryption HTTP_ENCRYPTION = config.server.http.encryption; + public static final Policies HTTP_POLICIES = config.server.http.policies; public static final GameOptions GAME_OPTIONS = config.server.game.gameOptions; public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits; diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 73e761e6e..bddfa9964 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -3,10 +3,18 @@ package emu.grasscutter; import java.io.*; import java.util.Calendar; +import emu.grasscutter.auth.AuthenticationSystem; +import emu.grasscutter.auth.DefaultAuthentication; import emu.grasscutter.command.CommandMap; import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; +import emu.grasscutter.server.http.HttpServer; +import emu.grasscutter.server.http.dispatch.DispatchHandler; +import emu.grasscutter.server.http.handlers.AnnouncementsHandler; +import emu.grasscutter.server.http.handlers.GenericHandler; +import emu.grasscutter.server.http.handlers.LogHandler; +import emu.grasscutter.server.http.dispatch.RegionHandler; import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.Utils; import org.jline.reader.EndOfFileException; @@ -47,8 +55,10 @@ public final class Grasscutter { private static int day; // Current day of week. private static DispatchServer dispatchServer; + private static HttpServer httpServer; private static GameServer gameServer; private static PluginManager pluginManager; + private static AuthenticationSystem authenticationSystem; public static final Reflections reflector = new Reflections("emu.grasscutter"); public static ConfigContainer config; @@ -98,14 +108,27 @@ public final class Grasscutter { // Initialize database. DatabaseManager.initialize(); + + // Initialize the default authentication system. + authenticationSystem = new DefaultAuthentication(); // Create server instances. dispatchServer = new DispatchServer(); + httpServer = new HttpServer(); gameServer = new GameServer(); // Create a server hook instance with both servers. new ServerHook(gameServer, dispatchServer); + // Create plugin manager instance. pluginManager = new PluginManager(); + // Add HTTP routes after loading plugins. + httpServer.addRouter(HttpServer.UnhandledRequestRouter.class); + httpServer.addRouter(HttpServer.DefaultRequestRouter.class); + httpServer.addRouter(RegionHandler.class); + httpServer.addRouter(LogHandler.class); + httpServer.addRouter(GenericHandler.class); + httpServer.addRouter(AnnouncementsHandler.class); + httpServer.addRouter(DispatchHandler.class); // Start servers. var runMode = SERVER.runMode; @@ -114,6 +137,7 @@ public final class Grasscutter { gameServer.start(); } else if (runMode == ServerRunMode.DISPATCH_ONLY) { dispatchServer.start(); + httpServer.start(); } else if (runMode == ServerRunMode.GAME_ONLY) { gameServer.start(); } else { @@ -141,6 +165,19 @@ public final class Grasscutter { pluginManager.disablePlugins(); } + /* + * Methods for the language system component. + */ + + public static void loadLanguage() { + var locale = config.language.language; + language = Language.getLanguage(Utils.getLanguageCode(locale)); + } + + /* + * Methods for the configuration system component. + */ + /** * Attempts to load the configuration from a file. */ @@ -157,11 +194,6 @@ public final class Grasscutter { } } - public static void loadLanguage() { - var locale = config.language.language; - language = Language.getLanguage(Utils.getLanguageCode(locale)); - } - /** * Saves the provided server configuration. * @param config The configuration to save, or null for a new one. @@ -178,44 +210,10 @@ public final class Grasscutter { } } - public static void startConsole() { - // Console should not start in dispatch only mode. - if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { - getLogger().info(translate("messages.dispatch.no_commands_error")); - return; - } - - getLogger().info(translate("messages.status.done")); - String input = null; - boolean isLastInterrupted = false; - while (true) { - try { - input = consoleLineReader.readLine("> "); - } catch (UserInterruptException e) { - if (!isLastInterrupted) { - isLastInterrupted = true; - Grasscutter.getLogger().info("Press Ctrl-C again to shutdown."); - continue; - } else { - Runtime.getRuntime().exit(0); - } - } catch (EndOfFileException e) { - Grasscutter.getLogger().info("EOF detected."); - continue; - } catch (IOError e) { - Grasscutter.getLogger().error("An IO error occurred.", e); - continue; - } - - isLastInterrupted = false; - try { - CommandMap.getInstance().invoke(null, null, input); - } catch (Exception e) { - Grasscutter.getLogger().error(translate("messages.game.command_error"), e); - } - } - } - + /* + * Getters for the various server components. + */ + public static ConfigContainer getConfig() { return config; } @@ -271,16 +269,74 @@ public final class Grasscutter { public static PluginManager getPluginManager() { return pluginManager; } - - public static void updateDayOfWeek() { - Calendar calendar = Calendar.getInstance(); - day = calendar.get(Calendar.DAY_OF_WEEK); + + public static AuthenticationSystem getAuthenticationSystem() { + return authenticationSystem; } public static int getCurrentDayOfWeek() { return day; } + + /* + * Utility methods. + */ + + public static void updateDayOfWeek() { + Calendar calendar = Calendar.getInstance(); + day = calendar.get(Calendar.DAY_OF_WEEK); + } + public static void startConsole() { + // Console should not start in dispatch only mode. + if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { + getLogger().info(translate("messages.dispatch.no_commands_error")); + return; + } + + getLogger().info(translate("messages.status.done")); + String input = null; + boolean isLastInterrupted = false; + while (true) { + try { + input = consoleLineReader.readLine("> "); + } catch (UserInterruptException e) { + if (!isLastInterrupted) { + isLastInterrupted = true; + Grasscutter.getLogger().info("Press Ctrl-C again to shutdown."); + continue; + } else { + Runtime.getRuntime().exit(0); + } + } catch (EndOfFileException e) { + Grasscutter.getLogger().info("EOF detected."); + continue; + } catch (IOError e) { + Grasscutter.getLogger().error("An IO error occurred.", e); + continue; + } + + isLastInterrupted = false; + try { + CommandMap.getInstance().invoke(null, null, input); + } catch (Exception e) { + Grasscutter.getLogger().error(translate("messages.game.command_error"), e); + } + } + } + + /** + * Sets the authentication system for the server. + * @param authenticationSystem The authentication system to use. + */ + public static void setAuthenticationSystem(AuthenticationSystem authenticationSystem) { + Grasscutter.authenticationSystem = authenticationSystem; + } + + /* + * Enums for the configuration. + */ + public enum ServerRunMode { HYBRID, DISPATCH_ONLY, GAME_ONLY } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 4e09f8881..8b8a9a185 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -117,7 +117,7 @@ public final class DispatchServer { .setTitle(DISPATCH_INFO.defaultName) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://" + "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/query_cur_region/" + defaultServerName) @@ -150,7 +150,7 @@ public final class DispatchServer { .setTitle(regionInfo.Title) .setType("DEV_PUBLIC") .setDispatchUrl( - "http" + (DISPATCH_ENCRYPTION.useInRouting ? "s" : "") + "://" + "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + "/query_cur_region/" + regionInfo.Name) @@ -189,14 +189,14 @@ public final class DispatchServer { Server server = new Server(); ServerConnector serverConnector; - if(DISPATCH_ENCRYPTION.useEncryption) { + if(HTTP_ENCRYPTION.useEncryption) { SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - File keystoreFile = new File(DISPATCH_ENCRYPTION.keystore); + File keystoreFile = new File(HTTP_ENCRYPTION.keystore); if(keystoreFile.exists()) { try { sslContextFactory.setKeyStorePath(keystoreFile.getPath()); - sslContextFactory.setKeyStorePassword(DISPATCH_ENCRYPTION.keystorePassword); + sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword); } catch (Exception e) { e.printStackTrace(); Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error")); @@ -214,7 +214,7 @@ public final class DispatchServer { serverConnector = new ServerConnector(server, sslContextFactory); } else { Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); - DISPATCH_ENCRYPTION.useEncryption = false; + HTTP_ENCRYPTION.useEncryption = false; serverConnector = new ServerConnector(server); } @@ -227,18 +227,19 @@ public final class DispatchServer { return server; }); - config.enforceSsl = DISPATCH_ENCRYPTION.useEncryption; + config.enforceSsl = HTTP_ENCRYPTION.useEncryption; if(SERVER.debugLevel == ServerDebugMode.ALL) { config.enableDevLogging(); } - if (DISPATCH_POLICIES.cors.enabled) { - var corsPolicy = DISPATCH_POLICIES.cors; + if (HTTP_POLICIES.cors.enabled) { + var corsPolicy = HTTP_POLICIES.cors; if (corsPolicy.allowedOrigins.length > 0) config.enableCorsForOrigin(corsPolicy.allowedOrigins); else config.enableCorsForAllOrigins(); } }); + httpServer.get("/", (req, res) -> res.send("" + translate("messages.status.welcome") + "")); httpServer.raw().error(404, ctx -> { @@ -279,6 +280,7 @@ public final class DispatchServer { res.send(event.getRegionList()); }); + // /server/:id -> 2.6.5x httpServer.get("/query_cur_region/:id", (req, res) -> { String regionName = req.params("id"); // Log diff --git a/src/main/java/emu/grasscutter/server/http/HttpServer.java b/src/main/java/emu/grasscutter/server/http/HttpServer.java new file mode 100644 index 000000000..dc0d396a6 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/HttpServer.java @@ -0,0 +1,176 @@ +package emu.grasscutter.server.http; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerDebugMode; +import express.Express; +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 static emu.grasscutter.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(SERVER.debugLevel == 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. + */ + public void start() { + // Attempt to start the HTTP server. + 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("/", (req, res) -> res.send(""" + + + + + + %s + + """.formatted(translate("messages.status.welcome")))); + } + } + + /** + * 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(SERVER.debugLevel == ServerDebugMode.MISSING) + Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url())); + context.contentType("text/html"); + context.result(""" + + + + + + + + + + + """); + }); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/http/Router.java b/src/main/java/emu/grasscutter/server/http/Router.java new file mode 100644 index 000000000..1720d7ca0 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/Router.java @@ -0,0 +1,16 @@ +package emu.grasscutter.server.http; + +import express.Express; +import io.javalin.Javalin; + +/** + * Defines routes for an {@link Express} instance. + */ +public interface Router { + + /** + * Called when the router is initialized by Express. + * @param express An Express instance. + */ + void applyRoutes(Express express, Javalin handle); +} diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java new file mode 100644 index 000000000..22a31fe6a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java @@ -0,0 +1,100 @@ +package emu.grasscutter.server.http.dispatch; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.auth.AuthenticationSystem; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.server.http.objects.*; +import emu.grasscutter.server.http.objects.ComboTokenReqJson.LoginTokenData; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; + +import static emu.grasscutter.utils.Language.translate; + +/** + * Handles requests related to authentication. (aka dispatch) + */ +public final class DispatchHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // Username & Password login (from client). + express.post("/hk4e_global/mdk/shield/api/login", DispatchHandler::clientLogin); + // Cached token login (from registry). + express.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin); + // Combo token login (from session key). + express.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin); + } + + /** + * @route /hk4e_global/mdk/shield/api/login + */ + private static void clientLogin(Request request, Response response) { + // Parse body data. + String rawBodyData = request.ctx().body(); + var bodyData = Utils.jsonDecode(rawBodyData, LoginAccountRequestJson.class); + + // Validate body data. + if(bodyData == null) + return; + + // Pass data to authentication handler. + var responseData = Grasscutter.getAuthenticationSystem() + .getPasswordAuthenticator() + .authenticate(AuthenticationSystem.fromPasswordRequest(request, bodyData)); + // Send response. + response.send(responseData); + + // Log to console. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); + } + + /** + * @route /hk4e_global/mdk/shield/api/verify + */ + private static void tokenLogin(Request request, Response response) { + // Parse body data. + String rawBodyData = request.ctx().body(); + var bodyData = Utils.jsonDecode(rawBodyData, LoginTokenRequestJson.class); + + // Validate body data. + if(bodyData == null) + return; + + // Pass data to authentication handler. + var responseData = Grasscutter.getAuthenticationSystem() + .getTokenAuthenticator() + .authenticate(AuthenticationSystem.fromTokenRequest(request, bodyData)); + // Send response. + response.send(responseData); + + // Log to console. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); + } + + /** + * @route /hk4e_global/combo/granter/login/v2/login + */ + private static void sessionKeyLogin(Request request, Response response) { + // Parse body data. + String rawBodyData = request.ctx().body(); + var bodyData = Utils.jsonDecode(rawBodyData, ComboTokenReqJson.class); + + // Validate body data. + if(bodyData == null || bodyData.data == null) + return; + + // Decode additional body data. + var tokenData = Utils.jsonDecode(bodyData.data, LoginTokenData.class); + + // Pass data to authentication handler. + var responseData = Grasscutter.getAuthenticationSystem() + .getSessionKeyAuthenticator() + .authenticate(AuthenticationSystem.fromComboTokenRequest(request, bodyData, tokenData)); + // Send response. + response.send(responseData); + + // Log to console. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", request.ip())); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java new file mode 100644 index 000000000..e720a4b15 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -0,0 +1,186 @@ +package emu.grasscutter.server.http.dispatch; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.Grasscutter.ServerRunMode; +import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*; +import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; +import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; +import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; + +import java.io.File; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*; + +/** + * Handles requests related to region queries. + */ +public final class RegionHandler implements Router { + private String regionQuery = ""; + private String regionList = ""; + + private static final Map regions = new ConcurrentHashMap<>(); + private static String regionListResponse; + + public RegionHandler() { + try { // Read & initialize region data. + this.readRegionData(); + this.initialize(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to initialize region data.", exception); + } + } + + /** + * Loads initial region data. + */ + private void readRegionData() { + File file; + + file = new File(DATA("query_region_list.txt")); + if (file.exists()) + this.regionList = new String(FileUtils.read(file)); + else Grasscutter.getLogger().error("[Dispatch] 'query_region_list' not found!"); + + file = new File(DATA("query_cur_region.txt")); + if (file.exists()) + regionQuery = new String(FileUtils.read(file)); + else Grasscutter.getLogger().warn("[Dispatch] 'query_cur_region' not found!"); + } + + /** + * Configures region data according to configuration. + */ + private void initialize() throws InvalidProtocolBufferException { + // Decode the initial region query. + byte[] queryBase64 = Base64.getDecoder().decode(this.regionQuery); + QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(queryBase64); + + // Create regions. + List servers = new ArrayList<>(); + List usedNames = new ArrayList<>(); // List to check for potential naming conflicts. + + var configuredRegions = new ArrayList<>(List.of(DISPATCH_INFO.regions)); + if(SERVER.runMode != ServerRunMode.HYBRID && configuredRegions.size() == 0) { + Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state."); + System.exit(1); + } else configuredRegions.add(new Region("os_usa", DISPATCH_INFO.defaultName, + lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress), + lr(GAME_INFO.accessPort, GAME_INFO.bindPort))); + + configuredRegions.forEach(region -> { + if (usedNames.contains(region.Name)) { + Grasscutter.getLogger().error("Region name already in use."); + return; + } + + // Create a region identifier. + var identifier = RegionSimpleInfo.newBuilder() + .setName(region.Name).setTitle(region.Title) + .setType("DEV_PUBLIC").setDispatchUrl( + "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + + "/query_cur_region/" + region.Name) + .build(); + usedNames.add(region.Name); servers.add(identifier); + + // Create a region info object. + var regionInfo = regionQuery.getRegionInfo().toBuilder() + .setGateserverIp(region.Ip).setGateserverPort(region.Port) + .setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin"))) + .build(); + // Create an updated region query. + var updatedQuery = regionQuery.toBuilder().setRegionInfo(regionInfo).build(); + regions.put(region.Name, new RegionData(updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray()))); + }); + + // Decode the initial region list. + byte[] listBase64 = Base64.getDecoder().decode(this.regionList); + QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.parseFrom(listBase64); + + // Create an updated region list. + QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder() + .addAllRegionList(servers) + .setClientSecretKey(regionList.getClientSecretKey()) + .setClientCustomConfigEncrypted(regionList.getClientCustomConfigEncrypted()) + .setEnableLoginPc(true).build(); + + // Set the region list response. + regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray()); + } + + @Override public void applyRoutes(Express express, Javalin handle) { + express.get("/query_region_list", RegionHandler::queryRegionList); + express.get("/query_cur_region/:region", RegionHandler::queryCurrentRegion ); + } + + /** + * @route /query_region_list + */ + private static void queryRegionList(Request request, Response response) { + // Invoke event. + QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse); event.call(); + // Respond with event result. + response.send(event.getRegionList()); + + // Log to console. + Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", request.ip())); + } + + /** + * @route /query_cur_region/:region + */ + private static void queryCurrentRegion(Request request, Response response) { + // Get region to query. + String regionName = request.params("region"); + + // Get region data. + String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; + if (request.query().values().size() > 0) + regionData = regions.get(regionName).getBase64(); + + // Invoke event. + QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); + // Respond with event result. + response.send(event.getRegionInfo()); + + // Log to console. + Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region/%s", request.ip(), regionName)); + } + + /** + * Region data container. + */ + public static class RegionData { + private final QueryCurrRegionHttpRsp regionQuery; + private final String base64; + + public RegionData(QueryCurrRegionHttpRsp prq, String b64) { + this.regionQuery = prq; + this.base64 = b64; + } + + public QueryCurrRegionHttpRsp getRegionQuery() { + return this.regionQuery; + } + + public String getBase64() { + return this.base64; + } + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java new file mode 100644 index 000000000..a64e0552a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java @@ -0,0 +1,58 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.server.http.Router; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Objects; + +import static emu.grasscutter.Configuration.DATA; + +/** + * Handles requests related to the announcements page. + */ +public final class AnnouncementsHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement); + // hk4e-api-os-static.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement); + // hk4e-sdk-os.hoyoverse.com + express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); + } + + private static void getAnnouncement(Request request, Response response) { + if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(DATA("GameAnnouncement.json"))) +"}"); + } else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { + String data = readToString(new File(DATA("GameAnnouncementList.json"))).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis())); + response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}"); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private static String readToString(File file) { + long length = file.length(); + byte[] content = new byte[(int) length]; + + try { + FileInputStream in = new FileInputStream(file); + in.read(content); in.close(); + } catch (IOException ignored) { + Grasscutter.getLogger().warn("File not found: " + file.getAbsolutePath()); + } + + return new String(content); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java new file mode 100644 index 000000000..bb0bc8eea --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java @@ -0,0 +1,47 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.server.http.Router; +import express.Express; +import io.javalin.Javalin; + +/** + * Handles all generic, hard-coded responses. + */ +public final class GenericHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // hk4e-sdk-os.hoyoverse.com + express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); + // hk4e-sdk-os.hoyoverse.com + // this could be either GET or POST based on the observation of different clients + express.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"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 + express.post("/account/risky/api/check", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); + + // sdk-os-static.hoyoverse.com + express.get("/combo/box/api/config/sdk/combo", new DispatchHttpJsonHandler("{\"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 + express.get("/hk4e_global/combo/granter/api/getConfig", new DispatchHttpJsonHandler("{\"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 + express.get("/hk4e_global/mdk/shield/api/loadConfig", new DispatchHttpJsonHandler("{\"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? + // abtest-api-data-sg.hoyoverse.com + express.post("/data_abtest_api/config/experiment/list", new DispatchHttpJsonHandler("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}")); + + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); + // hk4e-api-os.hoyoverse.com + express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + + // log-upload-os.mihoyo.com + express.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); + express.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); + express.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); + // /perf/config/verify?device_id=xxx&platform=x&name=xxx + express.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); + + // webstatic-sea.hoyoverse.com + express.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java new file mode 100644 index 000000000..4f52c0826 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java @@ -0,0 +1,18 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.server.dispatch.ClientLogHandler; +import emu.grasscutter.server.http.Router; +import express.Express; +import io.javalin.Javalin; + +/** + * Handles logging requests made to the server. + */ +public final class LogHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + // overseauspider.yuanshen.com + express.post("/log", new ClientLogHandler()); + // log-upload-os.mihoyo.com + express.post("/crash/dataUpload", new ClientLogHandler()); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java new file mode 100644 index 000000000..5642f159a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenReqJson.java @@ -0,0 +1,15 @@ +package emu.grasscutter.server.http.objects; + +public class ComboTokenReqJson { + public int app_id; + public int channel_id; + public String data; + public String device; + public String sign; + + public static class LoginTokenData { + public String uid; + public String token; + public boolean guest; + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java new file mode 100644 index 000000000..b592fa163 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/ComboTokenResJson.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.http.objects; + +public class ComboTokenResJson { + public String message; + public int retcode; + public LoginData data = new LoginData(); + + public static class LoginData { + public int account_type = 1; + public boolean heartbeat; + public String combo_id; + public String combo_token; + public String open_id; + public String data = "{\"guest\":false}"; + public String fatigue_remind = null; // ? + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java b/src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java new file mode 100644 index 000000000..3a8193a97 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/LoginAccountRequestJson.java @@ -0,0 +1,7 @@ +package emu.grasscutter.server.http.objects; + +public class LoginAccountRequestJson { + public String account; + public String password; + public boolean is_crypto; +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java b/src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java new file mode 100644 index 000000000..5601c1c29 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/LoginResultJson.java @@ -0,0 +1,38 @@ +package emu.grasscutter.server.http.objects; + +public class LoginResultJson { + public String message; + public int retcode; + public VerifyData data = new VerifyData(); + + public static class VerifyData { + public VerifyAccountData account = new VerifyAccountData(); + public boolean device_grant_required = false; + public String realname_operation = "NONE"; + public boolean realperson_required = false; + public boolean safe_mobile_required = false; + } + + public static class VerifyAccountData { + public String uid; + public String name = ""; + public String email = ""; + public String mobile = ""; + public String is_email_verify = "0"; + public String realname = ""; + public String identity_card = ""; + public String token; + public String safe_mobile = ""; + public String facebook_name = ""; + public String twitter_name = ""; + public String game_center_name = ""; + public String google_name = ""; + public String apple_name = ""; + public String sony_name = ""; + public String tap_name = ""; + public String country = "US"; + public String reactivate_ticket = ""; + public String area_code = "**"; + public String device_grant_ticket = ""; + } +} diff --git a/src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java b/src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java new file mode 100644 index 000000000..d01c60401 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/objects/LoginTokenRequestJson.java @@ -0,0 +1,6 @@ +package emu.grasscutter.server.http.objects; + +public class LoginTokenRequestJson { + public String uid; + public String token; +} diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index 76556700c..5a06b90be 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -96,8 +96,10 @@ public class ConfigContainer { public ServerDebugMode debugLevel = ServerDebugMode.NONE; public ServerRunMode runMode = ServerRunMode.HYBRID; - public Dispatch dispatch = new Dispatch(); + public HTTP http = new HTTP(); public Game game = new Game(); + + public Dispatch dispatch = new Dispatch(); } public static class Language { @@ -111,8 +113,8 @@ public class ConfigContainer { } /* Server options. */ - - public static class Dispatch { + + public static class HTTP { public String bindAddress = "0.0.0.0"; /* This is the address used in URLs. */ public String accessAddress = "127.0.0.1"; @@ -120,12 +122,9 @@ public class ConfigContainer { public int bindPort = 443; /* This is the port used in URLs. */ public int accessPort = 0; - + public Encryption encryption = new Encryption(); public Policies policies = new Policies(); - public Region[] regions = {}; - - public String defaultName = "Grasscutter"; } public static class Game { @@ -144,6 +143,12 @@ public class ConfigContainer { /* Data containers. */ + public static class Dispatch { + public Region[] regions = {}; + + public String defaultName = "Grasscutter"; + } + public static class Encryption { public boolean useEncryption = true; /* Should 'https' be appended to URLs? */ @@ -226,6 +231,18 @@ public class ConfigContainer { /* Objects. */ public static class Region { + public Region() { } + + public Region( + String name, String title, + String address, int port + ) { + this.Name = name; + this.Title = title; + this.Ip = address; + this.Port = port; + } + public String Name = "os_usa"; public String Title = "Grasscutter"; public String Ip = "127.0.0.1"; diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index c9c3c0c70..b23f2913d 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -33,7 +33,8 @@ "session_key_error": "Wrong session key.", "username_error": "Username not found.", "username_create_error": "Username not found, create failed." - } + }, + "router_error": "[Dispatch] Unable to attach router." }, "status": { "free_software": "Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter", From 3adf0d448c3a55a7ec36de532461bfe6af4d3557 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 23:22:30 -0400 Subject: [PATCH 07/12] Refactor dispatch (now called HTTP) server (pt. 2) --- .../java/emu/grasscutter/Grasscutter.java | 18 +- .../command/commands/ReloadCommand.java | 2 +- .../grasscutter/game/gacha/GachaBanner.java | 6 +- .../grasscutter/plugin/api/ServerHook.java | 43 +- .../scripts/serializer/LuaSerializer.java | 6 +- .../server/dispatch/AnnouncementHandler.java | 38 -- .../server/dispatch/ClientLogHandler.java | 19 - .../server/dispatch/DispatchServer.java | 532 ------------------ .../authentication/AuthenticationHandler.java | 16 - .../DefaultAuthenticationHandler.java | 80 --- .../dispatch/http/GachaRecordHandler.java | 54 -- .../dispatch/json/ComboTokenReqJson.java | 15 - .../dispatch/json/ComboTokenResJson.java | 17 - .../json/LoginAccountRequestJson.java | 7 - .../server/dispatch/json/LoginResultJson.java | 38 -- .../dispatch/json/LoginTokenRequestJson.java | 6 - .../grasscutter/server/http/HttpServer.java | 4 +- .../server/http/dispatch/RegionHandler.java | 14 +- .../http/handlers/AnnouncementsHandler.java | 8 +- .../server/http/handlers/GachaHandler.java | 71 +++ .../server/http/handlers/GenericHandler.java | 44 +- .../http/handlers/LegacyAuthHandler.java | 17 + .../server/http/handlers/LogHandler.java | 12 +- .../objects/HttpJsonResponse.java} | 6 +- .../packet/send/PacketPlayerLoginRsp.java | 4 +- 25 files changed, 195 insertions(+), 882 deletions(-) delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java delete mode 100644 src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java create mode 100644 src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java rename src/main/java/emu/grasscutter/server/{dispatch/DispatchHttpJsonHandler.java => http/objects/HttpJsonResponse.java} (90%) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index bddfa9964..30768bcb5 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -11,9 +11,7 @@ import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.server.http.HttpServer; import emu.grasscutter.server.http.dispatch.DispatchHandler; -import emu.grasscutter.server.http.handlers.AnnouncementsHandler; -import emu.grasscutter.server.http.handlers.GenericHandler; -import emu.grasscutter.server.http.handlers.LogHandler; +import emu.grasscutter.server.http.handlers.*; import emu.grasscutter.server.http.dispatch.RegionHandler; import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.Utils; @@ -33,7 +31,6 @@ import ch.qos.logback.classic.Logger; import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.database.DatabaseManager; import emu.grasscutter.utils.Language; -import emu.grasscutter.server.dispatch.DispatchServer; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; @@ -54,7 +51,6 @@ public final class Grasscutter { private static int day; // Current day of week. - private static DispatchServer dispatchServer; private static HttpServer httpServer; private static GameServer gameServer; private static PluginManager pluginManager; @@ -113,11 +109,10 @@ public final class Grasscutter { authenticationSystem = new DefaultAuthentication(); // Create server instances. - dispatchServer = new DispatchServer(); httpServer = new HttpServer(); gameServer = new GameServer(); // Create a server hook instance with both servers. - new ServerHook(gameServer, dispatchServer); + new ServerHook(gameServer, httpServer); // Create plugin manager instance. pluginManager = new PluginManager(); @@ -129,14 +124,15 @@ public final class Grasscutter { httpServer.addRouter(GenericHandler.class); httpServer.addRouter(AnnouncementsHandler.class); httpServer.addRouter(DispatchHandler.class); + httpServer.addRouter(LegacyAuthHandler.class); + httpServer.addRouter(GachaHandler.class); // Start servers. var runMode = SERVER.runMode; if (runMode == ServerRunMode.HYBRID) { - dispatchServer.start(); + httpServer.start(); gameServer.start(); } else if (runMode == ServerRunMode.DISPATCH_ONLY) { - dispatchServer.start(); httpServer.start(); } else if (runMode == ServerRunMode.GAME_ONLY) { gameServer.start(); @@ -258,8 +254,8 @@ public final class Grasscutter { return gson; } - public static DispatchServer getDispatchServer() { - return dispatchServer; + public static HttpServer getHttpServer() { + return httpServer; } public static GameServer getGameServer() { diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 9414a89c4..984fd7d60 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -21,7 +21,7 @@ public final class ReloadCommand implements CommandHandler { Grasscutter.getGameServer().getGachaManager().load(); Grasscutter.getGameServer().getDropManager().load(); Grasscutter.getGameServer().getShopManager().load(); - Grasscutter.getDispatchServer().loadQueries(); + // Grasscutter.getHttpServer().loadQueries(); // Is this practical? CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done")); } diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index dce433fcf..7602f7c06 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -98,9 +98,9 @@ public class GachaBanner { } public GachaInfo toProto(String sessionKey) { - String record = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + String record = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; // Grasscutter.getLogger().info("record = " + record); GachaInfo.Builder info = GachaInfo.newBuilder() diff --git a/src/main/java/emu/grasscutter/plugin/api/ServerHook.java b/src/main/java/emu/grasscutter/plugin/api/ServerHook.java index a37abfb62..ffa19110d 100644 --- a/src/main/java/emu/grasscutter/plugin/api/ServerHook.java +++ b/src/main/java/emu/grasscutter/plugin/api/ServerHook.java @@ -1,10 +1,13 @@ package emu.grasscutter.plugin.api; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.auth.AuthenticationSystem; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.player.Player; -import emu.grasscutter.server.dispatch.DispatchServer; import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.server.http.HttpServer; +import emu.grasscutter.server.http.Router; import java.util.LinkedList; import java.util.List; @@ -15,7 +18,7 @@ import java.util.List; public final class ServerHook { private static ServerHook instance; private final GameServer gameServer; - private final DispatchServer dispatchServer; + private final HttpServer httpServer; /** * Gets the server hook instance. @@ -28,11 +31,11 @@ public final class ServerHook { /** * Hooks into a server. * @param gameServer The game server to hook into. - * @param dispatchServer The dispatch server to hook into. + * @param httpServer The HTTP server to hook into. */ - public ServerHook(GameServer gameServer, DispatchServer dispatchServer) { + public ServerHook(GameServer gameServer, HttpServer httpServer) { this.gameServer = gameServer; - this.dispatchServer = dispatchServer; + this.httpServer = httpServer; instance = this; } @@ -45,10 +48,10 @@ public final class ServerHook { } /** - * @return The dispatch server. + * @return The HTTP server. */ - public DispatchServer getDispatchServer() { - return this.dispatchServer; + public HttpServer getHttpServer() { + return this.httpServer; } /** @@ -70,4 +73,28 @@ public final class ServerHook { Command commandData = clazz.getAnnotation(Command.class); this.gameServer.getCommandMap().registerCommand(commandData.label(), handler); } + + /** + * Adds a router using an instance of a class. + * @param router A router instance. + */ + public void addRouter(Router router) { + this.addRouter(router.getClass()); + } + + /** + * Adds a router using a class. + * @param router The class of the router. + */ + public void addRouter(Class router) { + this.httpServer.addRouter(router); + } + + /** + * Sets the server's authentication system. + * @param authSystem An instance of the authentication system. + */ + public void setAuthSystem(AuthenticationSystem authSystem) { + Grasscutter.setAuthenticationSystem(authSystem); + } } \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java b/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java index a63328b55..8924a33ae 100644 --- a/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java +++ b/src/main/java/emu/grasscutter/scripts/serializer/LuaSerializer.java @@ -70,16 +70,14 @@ public class LuaSerializer implements Serializer { } try { + //noinspection ConfusingArgumentToVarargsMethod object = type.getDeclaredConstructor().newInstance(null); LuaValue[] keys = table.keys(); for (LuaValue k : keys) { try { Field field = object.getClass().getDeclaredField(k.checkjstring()); - if (field == null) { - continue; - } - + field.setAccessible(true); LuaValue keyValue = table.get(k); diff --git a/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java b/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java deleted file mode 100644 index ab752c8e1..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/AnnouncementHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package emu.grasscutter.server.dispatch; - -import emu.grasscutter.Grasscutter; -import express.http.HttpContextHandler; -import express.http.Request; -import express.http.Response; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Objects; - -import static emu.grasscutter.Configuration.*; - -public final class AnnouncementHandler implements HttpContextHandler { - @Override - public void handle(Request request, Response response) throws IOException {//event - if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) { - response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(DATA("GameAnnouncement.json"))) +"}"); - } else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) { - String data = readToString(new File(DATA("GameAnnouncementList.json"))).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis())); - response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}"); - } - } - @SuppressWarnings("ResultOfMethodCallIgnored") - private static String readToString(File file) { - long length = file.length(); - byte[] content = new byte[(int) length]; - try { - FileInputStream in = new FileInputStream(file); - in.read(content); in.close(); - } catch (IOException ignored) { - Grasscutter.getLogger().warn("File not found: " + file.getAbsolutePath()); - } - - return new String(content); - } -} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java b/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java deleted file mode 100644 index b3d48dbbb..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/ClientLogHandler.java +++ /dev/null @@ -1,19 +0,0 @@ -package emu.grasscutter.server.dispatch; - -import express.http.HttpContextHandler; -import express.http.Request; -import express.http.Response; - -import java.io.IOException; - -/** - * Used for processing crash dumps and logs generated by the game. - * Logs are in JSON, and are sent to the server for logging. - */ -public final class ClientLogHandler implements HttpContextHandler { - @Override - public void handle(Request request, Response response) throws IOException { - // TODO: Figure out how to dump request body and log to file. - response.send("{\"code\":0}"); - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java deleted file mode 100644 index 8b8a9a185..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ /dev/null @@ -1,532 +0,0 @@ -package emu.grasscutter.server.dispatch; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.protobuf.ByteString; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.Grasscutter.ServerDebugMode; -import emu.grasscutter.Grasscutter.ServerRunMode; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp; -import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp; -import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; -import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; -import emu.grasscutter.server.dispatch.authentication.AuthenticationHandler; -import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler; -import emu.grasscutter.server.dispatch.http.GachaRecordHandler; -import emu.grasscutter.server.dispatch.json.*; -import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; -import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; -import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; -import emu.grasscutter.tools.Tools; -import emu.grasscutter.utils.FileUtils; -import emu.grasscutter.utils.Utils; -import express.Express; -import io.javalin.http.staticfiles.Location; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import java.io.*; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static emu.grasscutter.utils.Language.translate; -import static emu.grasscutter.Configuration.*; - -public final class DispatchServer { - public static String query_region_list = ""; - public static String query_cur_region = ""; - - private final Gson gson; - private final String defaultServerName = "os_usa"; - - public String regionListBase64; - public Map regions; - private AuthenticationHandler authHandler; - private Express httpServer; - - public DispatchServer() { - this.regions = new HashMap<>(); - this.gson = new GsonBuilder().create(); - - this.loadQueries(); - this.initRegion(); - } - - public Express getServer() { - return httpServer; - } - - public void setHttpServer(Express httpServer) { - this.httpServer.stop(); - this.httpServer = httpServer; - this.httpServer.listen(DISPATCH_INFO.bindPort); - } - - public Gson getGsonFactory() { - return gson; - } - - public QueryCurrRegionHttpRsp getCurrRegion() { - // Needs to be fixed by having the game servers connect to the dispatch server. - if (SERVER.runMode == ServerRunMode.HYBRID) { - return regions.get(defaultServerName).parsedRegionQuery; - } - - Grasscutter.getLogger().warn("[Dispatch] Unsupported run mode for getCurrRegion()"); - return null; - } - - public void loadQueries() { - File file; - - file = new File(DATA("query_region_list.txt")); - if (file.exists()) { - query_region_list = new String(FileUtils.read(file)); - } else { - Grasscutter.getLogger().warn("[Dispatch] query_region_list not found! Using default region list."); - } - - file = new File(DATA("query_cur_region.txt")); - if (file.exists()) { - query_cur_region = new String(FileUtils.read(file)); - } else { - Grasscutter.getLogger().warn("[Dispatch] query_cur_region not found! Using default current region."); - } - } - - private void initRegion() { - try { - byte[] decoded = Base64.getDecoder().decode(query_region_list); - QueryRegionListHttpRsp rl = QueryRegionListHttpRsp.parseFrom(decoded); - - byte[] decoded2 = Base64.getDecoder().decode(query_cur_region); - QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2); - - List servers = new ArrayList<>(); - List usedNames = new ArrayList<>(); // List to check for potential naming conflicts. - if (SERVER.runMode == ServerRunMode.HYBRID) { // Automatically add the game server if in hybrid mode. - RegionSimpleInfo server = RegionSimpleInfo.newBuilder() - .setName("os_usa") - .setTitle(DISPATCH_INFO.defaultName) - .setType("DEV_PUBLIC") - .setDispatchUrl( - "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) - + "/query_cur_region/" + defaultServerName) - .build(); - usedNames.add(defaultServerName); - servers.add(server); - - RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() - .setGateserverIp(lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress)) - .setGateserverPort(lr(GAME_INFO.accessPort, GAME_INFO.bindPort)) - .setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin"))) - .build(); - - QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); - regions.put(defaultServerName, new RegionData(parsedRegionQuery, - Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()))); - - } else if (DISPATCH_INFO.regions.length == 0) { - Grasscutter.getLogger().error("[Dispatch] There are no game servers available. Exiting due to unplayable state."); - System.exit(1); - } - - for (var regionInfo : DISPATCH_INFO.regions) { - if (usedNames.contains(regionInfo.Name)) { - Grasscutter.getLogger().error("Region name already in use."); - continue; - } - RegionSimpleInfo server = RegionSimpleInfo.newBuilder() - .setName(regionInfo.Name) - .setTitle(regionInfo.Title) - .setType("DEV_PUBLIC") - .setDispatchUrl( - "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) - + "/query_cur_region/" + regionInfo.Name) - .build(); - usedNames.add(regionInfo.Name); - servers.add(server); - - RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder() - .setGateserverIp(regionInfo.Ip) - .setGateserverPort(regionInfo.Port) - .setSecretKey(ByteString - .copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin"))) - .build(); - - QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build(); - regions.put(regionInfo.Name, new RegionData(parsedRegionQuery, - Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray()))); - } - - QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.newBuilder() - .addAllRegionList(servers) - .setClientSecretKey(rl.getClientSecretKey()) - .setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted()) - .setEnableLoginPc(true) - .build(); - - this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray()); - } catch (Exception exception) { - Grasscutter.getLogger().error("[Dispatch] Error while initializing region info!", exception); - } - } - - public void start() throws Exception { - httpServer = new Express(config -> { - config.server(() -> { - Server server = new Server(); - ServerConnector serverConnector; - - if(HTTP_ENCRYPTION.useEncryption) { - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - File keystoreFile = new File(HTTP_ENCRYPTION.keystore); - - if(keystoreFile.exists()) { - try { - sslContextFactory.setKeyStorePath(keystoreFile.getPath()); - sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword); - } catch (Exception e) { - e.printStackTrace(); - 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 e2) { - Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error")); - e2.printStackTrace(); - } - } - - serverConnector = new ServerConnector(server, sslContextFactory); - } else { - Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error")); - HTTP_ENCRYPTION.useEncryption = false; - - serverConnector = new ServerConnector(server); - } - } else { - serverConnector = new ServerConnector(server); - } - - serverConnector.setPort(DISPATCH_INFO.bindPort); - server.setConnectors(new Connector[]{serverConnector}); - return server; - }); - - config.enforceSsl = HTTP_ENCRYPTION.useEncryption; - if(SERVER.debugLevel == ServerDebugMode.ALL) { - config.enableDevLogging(); - } - - if (HTTP_POLICIES.cors.enabled) { - var corsPolicy = HTTP_POLICIES.cors; - if (corsPolicy.allowedOrigins.length > 0) - config.enableCorsForOrigin(corsPolicy.allowedOrigins); - else config.enableCorsForAllOrigins(); - } - }); - - httpServer.get("/", (req, res) -> res.send("" + translate("messages.status.welcome") + "")); - - httpServer.raw().error(404, ctx -> { - if(SERVER.debugLevel == ServerDebugMode.MISSING) { - Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", ctx.method(), ctx.url())); - } - ctx.contentType("text/html"); - ctx.result(""); // I'm like 70% sure this won't break anything. - }); - - // Authentication Handler - // These routes are so that authentication routes are always the same no matter what auth system is used. - httpServer.get("/authentication/type", (req, res) -> { - res.send(this.getAuthHandler().getClass().getName()); - }); - - httpServer.post("/authentication/login", (req, res) -> this.getAuthHandler().handleLogin(req, res)); - httpServer.post("/authentication/register", (req, res) -> this.getAuthHandler().handleRegister(req, res)); - httpServer.post("/authentication/change_password", (req, res) -> this.getAuthHandler().handleChangePassword(req, res)); - - // Server Status - httpServer.get("/status/server", (req, res) -> { - - int playerCount = Grasscutter.getGameServer().getPlayers().size(); - String version = GameConstants.VERSION; - - res.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"version\":\"" + version + "\"}}"); - }); - - // Dispatch - httpServer.get("/query_region_list", (req, res) -> { - // Log - Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", req.ip())); - - // Invoke event. - QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListBase64); event.call(); - // Respond with event result. - res.send(event.getRegionList()); - }); - - // /server/:id -> 2.6.5x - httpServer.get("/query_cur_region/:id", (req, res) -> { - String regionName = req.params("id"); - // Log - Grasscutter.getLogger().info( - String.format("Client %s request: query_cur_region/%s", req.ip(), regionName)); - // Create a response form the request query parameters - String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; - if (req.query().values().size() > 0) { - response = regions.get(regionName).Base64; - } - - // Invoke event. - QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(response); event.call(); - // Respond with event result. - res.send(event.getRegionInfo()); - }); - - // Login - - httpServer.post("/hk4e_global/mdk/shield/api/login", (req, res) -> { - // Get post data - LoginAccountRequestJson requestData = null; - try { - String body = req.ctx().body(); - requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class); - } catch (Exception ignored) { } - - // Create response json - if (requestData == null) { - return; - } - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", req.ip())); - - res.send(this.getAuthHandler().handleGameLogin(req, requestData)); - }); - - // Login via token - httpServer.post("/hk4e_global/mdk/shield/api/verify", (req, res) -> { - // Get post data - LoginTokenRequestJson requestData = null; - try { - String body = req.ctx().body(); - requestData = getGsonFactory().fromJson(body, LoginTokenRequestJson.class); - } catch (Exception ignored) { - } - - // Create response json - if (requestData == null) { - return; - } - LoginResultJson responseData = new LoginResultJson(); - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", req.ip())); - - // Login - Account account = DatabaseHelper.getAccountById(requestData.uid); - - // Test - if (account == null || !account.getSessionKey().equals(requestData.token)) { - responseData.retcode = -111; - responseData.message = translate("messages.dispatch.account.account_cache_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", req.ip())); - } else { - responseData.message = "OK"; - responseData.data.account.uid = requestData.uid; - responseData.data.account.token = requestData.token; - responseData.data.account.email = account.getEmail(); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_success", req.ip(), requestData.uid)); - } - - res.send(responseData); - }); - - // Exchange for combo token - httpServer.post("/hk4e_global/combo/granter/login/v2/login", (req, res) -> { - // Get post data - ComboTokenReqJson requestData = null; - try { - String body = req.ctx().body(); - requestData = getGsonFactory().fromJson(body, ComboTokenReqJson.class); - } catch (Exception ignored) { - } - - // Create response json - if (requestData == null || requestData.data == null) { - return; - } - LoginTokenData loginData = getGsonFactory().fromJson(requestData.data, LoginTokenData.class); // Get login - // data - ComboTokenResJson responseData = new ComboTokenResJson(); - - // Login - Account account = DatabaseHelper.getAccountById(loginData.uid); - - // Test - if (account == null || !account.getSessionKey().equals(loginData.token)) { - responseData.retcode = -201; - responseData.message = translate("messages.dispatch.account.session_key_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", req.ip())); - } else { - responseData.message = "OK"; - responseData.data.open_id = loginData.uid; - responseData.data.combo_id = "157795300"; - responseData.data.combo_token = account.generateLoginToken(); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", req.ip())); - } - - res.send(responseData); - }); - - // TODO: There are some missing route request types here (You can tell if they are missing if they are .all and not anything else) - // When http requests for theses routes are found please remove it from the list in DispatchHttpJsonHandler and update the route request types here - - // Agreement and Protocol - // hk4e-sdk-os.hoyoverse.com - httpServer.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); - // hk4e-sdk-os.hoyoverse.com - // this could be either GET or POST based on the observation of different clients - httpServer.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"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\":\"\"}}}")); - - // Game data - // hk4e-api-os.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); - // hk4e-api-os.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); - // hk4e-api-os.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAnnList", new AnnouncementHandler()); - // hk4e-api-os-static.hoyoverse.com - httpServer.all("/common/hk4e_global/announcement/api/getAnnContent", new AnnouncementHandler()); - // hk4e-sdk-os.hoyoverse.com - httpServer.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); - - // Captcha - // api-account-os.hoyoverse.com - httpServer.post("/account/risky/api/check", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); - - // Config - // sdk-os-static.hoyoverse.com - httpServer.get("/combo/box/api/config/sdk/combo", new DispatchHttpJsonHandler("{\"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 - httpServer.get("/hk4e_global/combo/granter/api/getConfig", new DispatchHttpJsonHandler("{\"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 - httpServer.get("/hk4e_global/mdk/shield/api/loadConfig", new DispatchHttpJsonHandler("{\"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? - // abtest-api-data-sg.hoyoverse.com - httpServer.post("/data_abtest_api/config/experiment/list", new DispatchHttpJsonHandler("{\"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 - httpServer.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - httpServer.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - httpServer.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); - // /perf/config/verify?device_id=xxx&platform=x&name=xxx - httpServer.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); - - // Logging servers - // overseauspider.yuanshen.com - httpServer.all("/log", new ClientLogHandler()); - // log-upload-os.mihoyo.com - httpServer.all("/crash/dataUpload", new ClientLogHandler()); - - // webstatic-sea.hoyoverse.com - httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); - - // gacha record. - String gachaMappingsPath = Utils.toFilePath(DATA("/gacha_mappings.js")); - // TODO: Only serve the html page and have a subsequent request to fetch the gacha data. - httpServer.get("/gacha", new GachaRecordHandler()); - if(!(new File(gachaMappingsPath).exists())) { - Tools.createGachaMapping(gachaMappingsPath); - } - - httpServer.raw().config.addSinglePageRoot("/gacha/mappings", gachaMappingsPath, Location.EXTERNAL); - - // static file support for plugins - httpServer.raw().config.precompressStaticFiles = false; // If this isn't set to false, files such as images may appear corrupted when serving static files - - httpServer.listen(DISPATCH_INFO.bindPort); - Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(httpServer.raw().port()))); - } - - private Map parseQueryString(String qs) { - Map result = new HashMap<>(); - if (qs == null) { - return result; - } - - int last = 0, next, l = qs.length(); - while (last < l) { - next = qs.indexOf('&', last); - if (next == -1) { - next = l; - } - - if (next > last) { - int eqPos = qs.indexOf('=', last); - if (eqPos < 0 || eqPos > next) { - result.put(URLDecoder.decode(qs.substring(last, next), StandardCharsets.UTF_8), ""); - } else { - result.put(URLDecoder.decode(qs.substring(last, eqPos), StandardCharsets.UTF_8), - URLDecoder.decode(qs.substring(eqPos + 1, next), StandardCharsets.UTF_8)); - } - } - last = next + 1; - } - return result; - } - - public AuthenticationHandler getAuthHandler() { - if(authHandler == null) { - return new DefaultAuthenticationHandler(); - } - return authHandler; - } - - public boolean registerAuthHandler(AuthenticationHandler authHandler) { - if(this.authHandler != null) { - Grasscutter.getLogger().error(String.format("[Dispatch] Unable to register '%s' authentication handler. \n" + - "The '%s' authentication handler has already been registered", authHandler.getClass().getName(), this.authHandler.getClass().getName())); - return false; - } - this.authHandler = authHandler; - return true; - } - - public void resetAuthHandler() { - this.authHandler = null; - } - - public static class RegionData { - QueryCurrRegionHttpRsp parsedRegionQuery; - String Base64; - - public RegionData(QueryCurrRegionHttpRsp prq, String b64) { - this.parsedRegionQuery = prq; - this.Base64 = b64; - } - - public QueryCurrRegionHttpRsp getParsedRegionQuery() { - return parsedRegionQuery; - } - - public String getBase64() { - return Base64; - } - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java deleted file mode 100644 index 92a2961ea..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/AuthenticationHandler.java +++ /dev/null @@ -1,16 +0,0 @@ -package emu.grasscutter.server.dispatch.authentication; - -import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson; -import emu.grasscutter.server.dispatch.json.LoginResultJson; -import express.http.Request; -import express.http.Response; - -public interface AuthenticationHandler { - - // This is in case plugins also want some sort of authentication - void handleLogin(Request req, Response res); - void handleRegister(Request req, Response res); - void handleChangePassword(Request req, Response res); - - LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData); -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java b/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java deleted file mode 100644 index 67b3d4023..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/authentication/DefaultAuthenticationHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -package emu.grasscutter.server.dispatch.authentication; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson; -import emu.grasscutter.server.dispatch.json.LoginResultJson; -import express.http.Request; -import express.http.Response; - -import static emu.grasscutter.utils.Language.translate; -import static emu.grasscutter.Configuration.*; - -public class DefaultAuthenticationHandler implements AuthenticationHandler { - - @Override - public void handleLogin(Request req, Response res) { - res.send("Authentication is not available with the default authentication method"); - } - - @Override - public void handleRegister(Request req, Response res) { - res.send("Authentication is not available with the default authentication method"); - } - - @Override - public void handleChangePassword(Request req, Response res) { - res.send("Authentication is not available with the default authentication method"); - } - - @Override - public LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData) { - LoginResultJson responseData = new LoginResultJson(); - - // Login - Account account = DatabaseHelper.getAccountByName(requestData.account); - - // Check if account exists, else create a new one. - if (account == null) { - // Account doesn't exist, so we can either auto create it if the config value is set. - if (ACCOUNT.autoCreate) { - // This account has been created AUTOMATICALLY. There will be no permissions added. - account = DatabaseHelper.createAccountWithId(requestData.account, 0); - - for (String permission : ACCOUNT.defaultPermissions) { - account.addPermission(permission); - } - - if (account != null) { - responseData.message = "OK"; - responseData.data.account.uid = account.getId(); - responseData.data.account.token = account.generateSessionKey(); - responseData.data.account.email = account.getEmail(); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", req.ip(), responseData.data.account.uid)); - } else { - responseData.retcode = -201; - responseData.message = translate("messages.dispatch.account.username_create_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", req.ip())); - } - } else { - responseData.retcode = -201; - responseData.message = translate("messages.dispatch.account.username_error"); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", req.ip())); - } - } else { - // Account was found, log the player in - responseData.message = "OK"; - responseData.data.account.uid = account.getId(); - responseData.data.account.token = account.generateSessionKey(); - responseData.data.account.email = account.getEmail(); - - Grasscutter.getLogger().info(translate("messages.dispatch.account.login_success", req.ip(), responseData.data.account.uid)); - } - - return responseData; - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java b/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java deleted file mode 100644 index b90510367..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/http/GachaRecordHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package emu.grasscutter.server.dispatch.http; - -import java.io.File; -import java.io.IOException; - -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.Account; -import emu.grasscutter.utils.FileUtils; -import emu.grasscutter.utils.Utils; -import express.http.HttpContextHandler; -import express.http.Request; -import express.http.Response; - -import static emu.grasscutter.Configuration.*; - -public final class GachaRecordHandler implements HttpContextHandler { - String render_template; - public GachaRecordHandler() { - File template = new File(Utils.toFilePath(DATA("/gacha_records.html"))); - if (template.exists()) { - // Load from cache - render_template = new String(FileUtils.read(template)); - } else { - render_template = "{{REPLACE_RECORD}}"; - } - } - - @Override - public void handle(Request req, Response res) throws IOException { - // Grasscutter.getLogger().info( req.query().toString() ); - String sessionKey = req.query("s"); - int page = 0; - int gachaType = 0; - if (req.query("p") != null) { - page = Integer.parseInt(req.query("p")); - } - - if (req.query("gachaType") != null) { - gachaType = Integer.parseInt(req.query("gachaType")); - } - - Account account = DatabaseHelper.getAccountBySessionKey(sessionKey); - if (account != null) { - String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), page, gachaType).toString(); - // Grasscutter.getLogger().info(records); - String response = render_template.replace("{{REPLACE_RECORD}}", records) - .replace("{{REPLACE_MAXPAGE}}", String.valueOf(DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType))); - - res.send(response); - } else { - res.send("No account found."); - } - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java deleted file mode 100644 index b3497f8d4..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenReqJson.java +++ /dev/null @@ -1,15 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class ComboTokenReqJson { - public int app_id; - public int channel_id; - public String data; - public String device; - public String sign; - - public static class LoginTokenData { - public String uid; - public String token; - public boolean guest; - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java deleted file mode 100644 index 7c49d1278..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/ComboTokenResJson.java +++ /dev/null @@ -1,17 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class ComboTokenResJson { - public String message; - public int retcode; - public LoginData data = new LoginData(); - - public static class LoginData { - public int account_type = 1; - public boolean heartbeat; - public String combo_id; - public String combo_token; - public String open_id; - public String data = "{\"guest\":false}"; - public String fatigue_remind = null; // ? - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java deleted file mode 100644 index cb3aff349..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginAccountRequestJson.java +++ /dev/null @@ -1,7 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class LoginAccountRequestJson { - public String account; - public String password; - public boolean is_crypto; -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java deleted file mode 100644 index 1f4dcd4b4..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginResultJson.java +++ /dev/null @@ -1,38 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class LoginResultJson { - public String message; - public int retcode; - public VerifyData data = new VerifyData(); - - public static class VerifyData { - public VerifyAccountData account = new VerifyAccountData(); - public boolean device_grant_required = false; - public String realname_operation = "NONE"; - public boolean realperson_required = false; - public boolean safe_mobile_required = false; - } - - public static class VerifyAccountData { - public String uid; - public String name = ""; - public String email = ""; - public String mobile = ""; - public String is_email_verify = "0"; - public String realname = ""; - public String identity_card = ""; - public String token; - public String safe_mobile = ""; - public String facebook_name = ""; - public String twitter_name = ""; - public String game_center_name = ""; - public String google_name = ""; - public String apple_name = ""; - public String sony_name = ""; - public String tap_name = ""; - public String country = "US"; - public String reactivate_ticket = ""; - public String area_code = "**"; - public String device_grant_ticket = ""; - } -} diff --git a/src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java b/src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java deleted file mode 100644 index 12fed8f09..000000000 --- a/src/main/java/emu/grasscutter/server/dispatch/json/LoginTokenRequestJson.java +++ /dev/null @@ -1,6 +0,0 @@ -package emu.grasscutter.server.dispatch.json; - -public class LoginTokenRequestJson { - public String uid; - public String token; -} diff --git a/src/main/java/emu/grasscutter/server/http/HttpServer.java b/src/main/java/emu/grasscutter/server/http/HttpServer.java index dc0d396a6..5227d9793 100644 --- a/src/main/java/emu/grasscutter/server/http/HttpServer.java +++ b/src/main/java/emu/grasscutter/server/http/HttpServer.java @@ -162,11 +162,11 @@ public final class HttpServer { - + - + """); diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java index e720a4b15..8b5dbeec7 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java @@ -151,8 +151,10 @@ public final class RegionHandler implements Router { // Get region data. String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="; - if (request.query().values().size() > 0) - regionData = regions.get(regionName).getBase64(); + if (request.query().values().size() > 0) { + var region = regions.get(regionName); + if(region != null) regionData = region.getBase64(); + } // Invoke event. QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call(); @@ -183,4 +185,12 @@ public final class RegionHandler implements Router { return this.base64; } } + + /** + * Gets the current region query. + * @return A {@link QueryCurrRegionHttpRsp} object. + */ + public static QueryCurrRegionHttpRsp getCurrentRegion() { + return SERVER.runMode == ServerRunMode.HYBRID ? regions.get("os_usa").getRegionQuery() : null; + } } diff --git a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java index a64e0552a..794b88ed4 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/AnnouncementsHandler.java @@ -1,7 +1,7 @@ package emu.grasscutter.server.http.handlers; import emu.grasscutter.Grasscutter; -import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.server.http.objects.HttpJsonResponse; import emu.grasscutter.server.http.Router; import express.Express; import express.http.Request; @@ -21,15 +21,15 @@ import static emu.grasscutter.Configuration.DATA; public final class AnnouncementsHandler implements Router { @Override public void applyRoutes(Express express, Javalin handle) { // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); + express.all("/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + express.all("/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 express.all("/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement); // hk4e-api-os-static.hoyoverse.com express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement); // hk4e-sdk-os.hoyoverse.com - express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); + express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")); } private static void getAnnouncement(Request request, Response response) { diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java new file mode 100644 index 000000000..6cb27d90d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/GachaHandler.java @@ -0,0 +1,71 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.tools.Tools; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; +import io.javalin.http.staticfiles.Location; + +import java.io.File; + +import static emu.grasscutter.Configuration.DATA; + +/** + * Handles all gacha-related HTTP requests. + */ +public final class GachaHandler implements Router { + private final String gachaMappings; + + private static String frontendTemplate = "{{REPLACE_RECORD}}"; + + public GachaHandler() { + this.gachaMappings = Utils.toFilePath(DATA("/gacha_mappings.js")); + if(!(new File(this.gachaMappings).exists())) { + try { + Tools.createGachaMapping(this.gachaMappings); + } catch (Exception exception) { + Grasscutter.getLogger().warn("Failed to create gacha mappings.", exception); + } + } + + var templateFile = new File(DATA("/gacha_records.html")); + if(templateFile.exists()) + frontendTemplate = new String(FileUtils.read(templateFile)); + } + + @Override public void applyRoutes(Express express, Javalin handle) { + express.get("/gacha", GachaHandler::gachaRecords); + + express.useStaticFallback("/gacha/mappings", this.gachaMappings, Location.EXTERNAL); + } + + private static void gachaRecords(Request request, Response response) { + var sessionKey = request.query("s"); + + int page = 0, gachaType = 0; + if(request.query("p") != null) + page = Integer.parseInt(request.query("p")); + if(request.query("gachaType") != null) + gachaType = Integer.parseInt(request.query("gachaType")); + + // Get account from session key. + var account = DatabaseHelper.getAccountBySessionKey(sessionKey); + + if(account == null) // Send response. + response.status(404).send("Unable to find account."); + else { + String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), gachaType, page).toString(); + long maxPage = DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType); + + response.send(frontendTemplate + .replace("{{REPLACE_RECORD}}", records) + .replace("{{REPLACE_MAXPAGE}}", String.valueOf(maxPage))); + } + } +} diff --git a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java index bb0bc8eea..2de8969d7 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/GenericHandler.java @@ -1,8 +1,12 @@ package emu.grasscutter.server.http.handlers; -import emu.grasscutter.server.dispatch.DispatchHttpJsonHandler; +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.http.objects.HttpJsonResponse; import emu.grasscutter.server.http.Router; import express.Express; +import express.http.Request; +import express.http.Response; import io.javalin.Javalin; /** @@ -11,37 +15,41 @@ import io.javalin.Javalin; public final class GenericHandler implements Router { @Override public void applyRoutes(Express express, Javalin handle) { // hk4e-sdk-os.hoyoverse.com - express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); + express.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")); // hk4e-sdk-os.hoyoverse.com // this could be either GET or POST based on the observation of different clients - express.all("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"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\":\"\"}}}")); + 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\":\"\"}}}")); // api-account-os.hoyoverse.com - express.post("/account/risky/api/check", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); + express.post("/account/risky/api/check", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}")); // sdk-os-static.hoyoverse.com - express.get("/combo/box/api/config/sdk/combo", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}")); + 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\"}}}")); // hk4e-sdk-os-static.hoyoverse.com - express.get("/hk4e_global/combo/granter/api/getConfig", new DispatchHttpJsonHandler("{\"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}}")); + 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}}")); // hk4e-sdk-os-static.hoyoverse.com - express.get("/hk4e_global/mdk/shield/api/loadConfig", new DispatchHttpJsonHandler("{\"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}}}}")); + 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}}}}")); // Test api? // abtest-api-data-sg.hoyoverse.com - express.post("/data_abtest_api/config/experiment/list", new DispatchHttpJsonHandler("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}")); - - // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")); - // hk4e-api-os.hoyoverse.com - express.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")); + 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\"}}]}")); // log-upload-os.mihoyo.com - express.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - express.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}")); - express.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}")); + express.all("/log/sdk/upload", new HttpJsonResponse("{\"code\":0}")); + express.all("/sdk/upload", new HttpJsonResponse("{\"code\":0}")); + express.post("/sdk/dataUpload", new HttpJsonResponse("{\"code\":0}")); // /perf/config/verify?device_id=xxx&platform=x&name=xxx - express.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}")); + express.all("/perf/config/verify", new HttpJsonResponse("{\"code\":0}")); // webstatic-sea.hoyoverse.com - express.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}")); + express.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new HttpJsonResponse("{\"version\":51}")); + + express.get("/status/server", GenericHandler::serverStatus); + } + + private static void serverStatus(Request request, Response response) { + int playerCount = Grasscutter.getGameServer().getPlayers().size(); + String version = GameConstants.VERSION; + + response.send("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"version\":\"" + version + "\"}}"); } } diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java new file mode 100644 index 000000000..943a56d7e --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.http.handlers; + +import emu.grasscutter.server.http.Router; +import express.Express; +import io.javalin.Javalin; + +/** + * Handles the legacy authentication system routes. + */ +public final class LegacyAuthHandler implements Router { + @Override public void applyRoutes(Express express, Javalin handle) { + express.get("/authentication/type", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java index 4f52c0826..08025d365 100644 --- a/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java +++ b/src/main/java/emu/grasscutter/server/http/handlers/LogHandler.java @@ -1,8 +1,9 @@ package emu.grasscutter.server.http.handlers; -import emu.grasscutter.server.dispatch.ClientLogHandler; import emu.grasscutter.server.http.Router; import express.Express; +import express.http.Request; +import express.http.Response; import io.javalin.Javalin; /** @@ -11,8 +12,13 @@ import io.javalin.Javalin; public final class LogHandler implements Router { @Override public void applyRoutes(Express express, Javalin handle) { // overseauspider.yuanshen.com - express.post("/log", new ClientLogHandler()); + express.post("/log", LogHandler::log); // log-upload-os.mihoyo.com - express.post("/crash/dataUpload", new ClientLogHandler()); + express.post("/crash/dataUpload", LogHandler::log); + } + + private static void log(Request request, Response response) { + // TODO: Figure out how to dump request body and log to file. + response.send("{\"code\":0}"); } } diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java b/src/main/java/emu/grasscutter/server/http/objects/HttpJsonResponse.java similarity index 90% rename from src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java rename to src/main/java/emu/grasscutter/server/http/objects/HttpJsonResponse.java index 8d1164e8d..35ca9b006 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchHttpJsonHandler.java +++ b/src/main/java/emu/grasscutter/server/http/objects/HttpJsonResponse.java @@ -1,4 +1,4 @@ -package emu.grasscutter.server.dispatch; +package emu.grasscutter.server.http.objects; import java.io.IOException; import java.util.Arrays; @@ -13,7 +13,7 @@ import express.http.Response; import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.Configuration.*; -public final class DispatchHttpJsonHandler implements HttpContextHandler { +public final class HttpJsonResponse implements HttpContextHandler { 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 "/common/hk4e_global/announcement/api/getAlertPic", @@ -28,7 +28,7 @@ public final class DispatchHttpJsonHandler implements HttpContextHandler { "/crash/dataUpload" }; - public DispatchHttpJsonHandler(String response) { + public HttpJsonResponse(String response) { this.response = response; } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java index 9a21e4143..362493755 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -9,10 +9,12 @@ import emu.grasscutter.net.proto.PlayerLoginRspOuterClass.PlayerLoginRsp; import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass; import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.http.dispatch.RegionHandler; import emu.grasscutter.utils.FileUtils; import java.io.File; import java.util.Base64; +import java.util.Objects; import static emu.grasscutter.Configuration.*; @@ -55,7 +57,7 @@ public class PacketPlayerLoginRsp extends BasePacket { info = regionCache.getRegionInfo(); } else { - info = Grasscutter.getDispatchServer().getCurrRegion().getRegionInfo(); + info = Objects.requireNonNull(RegionHandler.getCurrentRegion()).getRegionInfo(); } PlayerLoginRsp p = PlayerLoginRsp.newBuilder() From 2e7cd0b46f2d64967c83e99913846e2f30d1908a Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:10:43 -0400 Subject: [PATCH 08/12] Fix errors --- src/main/java/emu/grasscutter/Grasscutter.java | 1 + src/main/java/emu/grasscutter/game/gacha/GachaBanner.java | 6 +++--- .../server/packet/send/PacketPlayerLoginRsp.java | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index fa37772ef..327aa174c 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -6,6 +6,7 @@ import java.util.Calendar; import emu.grasscutter.auth.AuthenticationSystem; import emu.grasscutter.auth.DefaultAuthentication; import emu.grasscutter.command.CommandMap; +import emu.grasscutter.game.managers.StaminaManager.StaminaManager; import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index 7b498a80f..d3b5d8959 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -140,9 +140,9 @@ public class GachaBanner { + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; - String details = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" - + lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" - + lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + String details = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://" + + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":" + + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort) + "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType; // Grasscutter.getLogger().info("record = " + record); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java index db66554f9..52a487d55 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerLoginRsp.java @@ -9,10 +9,13 @@ import emu.grasscutter.net.proto.PlayerLoginRspOuterClass.PlayerLoginRsp; import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass; import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.http.dispatch.RegionHandler; +import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.FileUtils; import java.io.File; import java.util.Base64; +import java.util.Objects; import static emu.grasscutter.Configuration.*; From 0dcf0862f8cd900fb4fc318ba234d3f53afdf97b Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:13:41 -0400 Subject: [PATCH 09/12] JavaDoc Fix --- src/main/java/emu/grasscutter/auth/AuthenticationSystem.java | 1 - src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java index 312fdad54..096c4124c 100644 --- a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -54,7 +54,6 @@ public interface AuthenticationSystem { /** * A data container that holds relevant data for authenticating a client. - * Call {@link AuthenticationRequest#builder()} to create a builder. */ @Builder @AllArgsConstructor @Getter class AuthenticationRequest { diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index 298d24493..0239b6e09 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -15,7 +15,7 @@ import static emu.grasscutter.utils.Language.translate; public final class DefaultAuthenticators { /** - * Handles the authentication request from the username & password form. + * Handles the authentication request from the username and password form. */ public static class PasswordAuthenticator implements Authenticator { @Override public LoginResultJson authenticate(AuthenticationRequest request) { From 5d7edc389e6449b21555b771682ed397c98cbc7d Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:21:55 -0400 Subject: [PATCH 10/12] Implement PR #657 --- .../java/emu/grasscutter/Configuration.java | 2 + .../grasscutter/server/http/HttpServer.java | 44 ++++++++++++++----- .../grasscutter/utils/ConfigContainer.java | 8 +++- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/main/java/emu/grasscutter/Configuration.java b/src/main/java/emu/grasscutter/Configuration.java index 52bfa65aa..4cbd0130c 100644 --- a/src/main/java/emu/grasscutter/Configuration.java +++ b/src/main/java/emu/grasscutter/Configuration.java @@ -1,6 +1,7 @@ package emu.grasscutter; import emu.grasscutter.utils.ConfigContainer; +import emu.grasscutter.utils.ConfigContainer.*; import java.util.Locale; @@ -40,6 +41,7 @@ public final class Configuration extends ConfigContainer { public static final Encryption HTTP_ENCRYPTION = config.server.http.encryption; public static final Policies HTTP_POLICIES = config.server.http.policies; + public static final Files HTTP_STATIC_FILES = config.server.http.files; public static final GameOptions GAME_OPTIONS = config.server.game.gameOptions; public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits; diff --git a/src/main/java/emu/grasscutter/server/http/HttpServer.java b/src/main/java/emu/grasscutter/server/http/HttpServer.java index 5227d9793..898a3a17e 100644 --- a/src/main/java/emu/grasscutter/server/http/HttpServer.java +++ b/src/main/java/emu/grasscutter/server/http/HttpServer.java @@ -2,13 +2,16 @@ 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.IOException; import static emu.grasscutter.Configuration.*; import static emu.grasscutter.utils.Language.translate; @@ -62,7 +65,7 @@ public final class HttpServer { var sslContextFactory = new SslContextFactory.Server(); var keystoreFile = new File(HTTP_ENCRYPTION.keystore); - if(!keystoreFile.exists()) {; + if(!keystoreFile.exists()) { HTTP_ENCRYPTION.useEncryption = false; HTTP_ENCRYPTION.useInRouting = false; @@ -137,15 +140,25 @@ public final class HttpServer { */ public static class DefaultRequestRouter implements Router { @Override public void applyRoutes(Express express, Javalin handle) { - express.get("/", (req, res) -> res.send(""" - - - - - - %s - - """.formatted(translate("messages.status.welcome")))); + express.get("/", (request, response) -> { + File file = new File(HTTP_STATIC_FILES.errorFile); + 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)); + } + }); } } @@ -158,7 +171,10 @@ public final class HttpServer { if(SERVER.debugLevel == ServerDebugMode.MISSING) Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url())); context.contentType("text/html"); - context.result(""" + + File file = new File(HTTP_STATIC_FILES.errorFile); + if(!file.exists()) + context.result(""" @@ -170,6 +186,12 @@ public final class HttpServer { """); + 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)); + } }); } } diff --git a/src/main/java/emu/grasscutter/utils/ConfigContainer.java b/src/main/java/emu/grasscutter/utils/ConfigContainer.java index 5a06b90be..b65fb10db 100644 --- a/src/main/java/emu/grasscutter/utils/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/utils/ConfigContainer.java @@ -17,7 +17,7 @@ import static emu.grasscutter.Grasscutter.config; */ public class ConfigContainer { private static int version() { - return 2; + return 3; } /** @@ -125,6 +125,7 @@ public class ConfigContainer { public Encryption encryption = new Encryption(); public Policies policies = new Policies(); + public Files files = new Files(); } public static class Game { @@ -227,6 +228,11 @@ public class ConfigContainer { public String nickName = "Server"; public String signature = "Welcome to Grasscutter!"; } + + public static class Files { + public String indexFile = "./index.html"; + public String errorFile = "./404.html"; + } /* Objects. */ From 04d9613facceebe270bd24dc404404c55646af5e Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:39:21 -0400 Subject: [PATCH 11/12] External authentication --- .../java/emu/grasscutter/Grasscutter.java | 2 -- .../auth/AuthenticationSystem.java | 20 +++++++++++ .../auth/DefaultAuthentication.java | 6 ++++ .../auth/DefaultAuthenticators.java | 20 +++++++++++ .../auth/ExternalAuthenticator.java | 33 +++++++++++++++++++ .../server/http/dispatch/DispatchHandler.java | 6 ++++ .../http/handlers/LegacyAuthHandler.java | 17 ---------- 7 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java delete mode 100644 src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 327aa174c..bc5144d97 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -35,7 +35,6 @@ import emu.grasscutter.utils.Language; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; -import emu.grasscutter.BuildConfig; import javax.annotation.Nullable; @@ -129,7 +128,6 @@ public final class Grasscutter { httpServer.addRouter(GenericHandler.class); httpServer.addRouter(AnnouncementsHandler.class); httpServer.addRouter(DispatchHandler.class); - httpServer.addRouter(LegacyAuthHandler.class); httpServer.addRouter(GachaHandler.class); // TODO: find a better place? diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java index 096c4124c..41aba1c8e 100644 --- a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -2,6 +2,7 @@ package emu.grasscutter.auth; import emu.grasscutter.server.http.objects.*; import express.http.Request; +import express.http.Response; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -52,12 +53,20 @@ public interface AuthenticationSystem { */ Authenticator getSessionKeyAuthenticator(); + /** + * This is the authenticator used for handling external authentication requests. + * @return An authenticator. + */ + ExternalAuthenticator getExternalAuthenticator(); + /** * A data container that holds relevant data for authenticating a client. */ @Builder @AllArgsConstructor @Getter class AuthenticationRequest { private final Request request; + @Nullable private final Response response; + @Nullable private final LoginAccountRequestJson passwordRequest; @Nullable private final LoginTokenRequestJson tokenRequest; @Nullable private final ComboTokenReqJson sessionKeyRequest; @@ -104,4 +113,15 @@ public interface AuthenticationSystem { .sessionKeyData(tokenData) .build(); } + + /** + * Generates an authentication request from a {@link Response} object. + * @param request The Express request. + * @param response the Express response. + * @return An authentication request. + */ + static AuthenticationRequest fromExternalRequest(Request request, Response response) { + return AuthenticationRequest.builder().request(request) + .response(response).build(); + } } diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java index b5e853cb0..08958d8e9 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -15,6 +15,7 @@ public final class DefaultAuthentication implements AuthenticationSystem { private final Authenticator passwordAuthenticator = new PasswordAuthenticator(); private final Authenticator tokenAuthenticator = new TokenAuthenticator(); private final Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); + private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication(); @Override public void createAccount(String username, String password) { @@ -46,4 +47,9 @@ public final class DefaultAuthentication implements AuthenticationSystem { public Authenticator getSessionKeyAuthenticator() { return this.sessionKeyAuthenticator; } + + @Override + public ExternalAuthenticator getExternalAuthenticator() { + return this.externalAuthenticator; + } } diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java index 0239b6e09..e1d5fddf0 100644 --- a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -158,4 +158,24 @@ public final class DefaultAuthenticators { return response; } } + + /** + * Handles authentication requests from external sources. + */ + public static class ExternalAuthentication implements ExternalAuthenticator { + @Override public void handleLogin(AuthenticationRequest request) { + assert request.getResponse() != null; + request.getResponse().send("Authentication is not available with the default authentication method."); + } + + @Override public void handleAccountCreation(AuthenticationRequest request) { + assert request.getResponse() != null; + request.getResponse().send("Authentication is not available with the default authentication method."); + } + + @Override public void handlePasswordReset(AuthenticationRequest request) { + assert request.getResponse() != null; + request.getResponse().send("Authentication is not available with the default authentication method."); + } + } } diff --git a/src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java b/src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java new file mode 100644 index 000000000..6bf78af6e --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/ExternalAuthenticator.java @@ -0,0 +1,33 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; + +/** + * Handles authentication via external routes. + */ +public interface ExternalAuthenticator { + + /** + * Called when an external login request is made. + * @param request The authentication request. + */ + void handleLogin(AuthenticationRequest request); + + /** + * Called when an external account creation request is made. + * @param request The authentication request. + * + * For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body. + * Use {@link AuthenticationRequest#getResponse()} to get the response body. + */ + void handleAccountCreation(AuthenticationRequest request); + + /** + * Called when an external password reset request is made. + * @param request The authentication request. + * + * For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body. + * Use {@link AuthenticationRequest#getResponse()} to get the response body. + */ + void handlePasswordReset(AuthenticationRequest request); +} diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java index 22a31fe6a..5b012c4c3 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java @@ -24,6 +24,12 @@ public final class DispatchHandler implements Router { express.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin); // Combo token login (from session key). express.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin); + + // External login (from other clients). + express.get("/authentication/type", (request, response) -> response.send(Grasscutter.getAuthenticationSystem().getClass().getSimpleName())); + express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); } /** diff --git a/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java b/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java deleted file mode 100644 index 943a56d7e..000000000 --- a/src/main/java/emu/grasscutter/server/http/handlers/LegacyAuthHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package emu.grasscutter.server.http.handlers; - -import emu.grasscutter.server.http.Router; -import express.Express; -import io.javalin.Javalin; - -/** - * Handles the legacy authentication system routes. - */ -public final class LegacyAuthHandler implements Router { - @Override public void applyRoutes(Express express, Javalin handle) { - express.get("/authentication/type", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - } -} \ No newline at end of file From 6ec27cd17a08571fced90364166139d1cb79b8f4 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sat, 14 May 2022 12:41:49 -0400 Subject: [PATCH 12/12] Update routes --- .../server/http/dispatch/DispatchHandler.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java index 5b012c4c3..5f9edcf0a 100644 --- a/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java +++ b/src/main/java/emu/grasscutter/server/http/dispatch/DispatchHandler.java @@ -27,9 +27,12 @@ public final class DispatchHandler implements Router { // External login (from other clients). express.get("/authentication/type", (request, response) -> response.send(Grasscutter.getAuthenticationSystem().getClass().getSimpleName())); - express.post("/authentication/login", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/register", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); - express.post("/authentication/change_password", (request, response) -> response.status(500).send("{\"notice\":\"This API is deprecated.\"}")); + express.post("/authentication/login", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() + .handleLogin(AuthenticationSystem.fromExternalRequest(request, response))); + express.post("/authentication/register", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() + .handleAccountCreation(AuthenticationSystem.fromExternalRequest(request, response))); + express.post("/authentication/change_password", (request, response) -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator() + .handlePasswordReset(AuthenticationSystem.fromExternalRequest(request, response))); } /**