Commit 5c3d9980 authored by Melledy's avatar Melledy Committed by GitHub
Browse files

Merge pull request #868 from Grasscutters/plugin-auth

Refactor 'dispatch' server/Update plugin authentication API
parents a751e71d 6ec27cd1
...@@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 ...@@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
group = 'xyz.grasscutters' group = 'xyz.grasscutters'
version = '1.1.1-dev' version = '1.1.2-dev'
sourceCompatibility = 17 sourceCompatibility = 17
...@@ -87,6 +87,9 @@ dependencies { ...@@ -87,6 +87,9 @@ dependencies {
implementation group: 'org.luaj', name: 'luaj-jse', version: '3.0.1' implementation group: 'org.luaj', name: 'luaj-jse', version: '3.0.1'
protobuf files('proto/') protobuf files('proto/')
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
} }
configurations.all { configurations.all {
......
package emu.grasscutter; package emu.grasscutter;
import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.ConfigContainer;
import emu.grasscutter.utils.ConfigContainer.*;
import java.util.Locale; import java.util.Locale;
...@@ -34,11 +35,13 @@ public final class Configuration extends ConfigContainer { ...@@ -34,11 +35,13 @@ public final class Configuration extends ConfigContainer {
public static final Database DATABASE = config.databaseInfo; public static final Database DATABASE = config.databaseInfo;
public static final Account ACCOUNT = config.account; 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 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 Encryption HTTP_ENCRYPTION = config.server.http.encryption;
public static final Policies DISPATCH_POLICIES = config.server.dispatch.policies; 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 GAME_OPTIONS = config.server.game.gameOptions;
public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits; public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits;
......
...@@ -3,11 +3,17 @@ package emu.grasscutter; ...@@ -3,11 +3,17 @@ package emu.grasscutter;
import java.io.*; import java.io.*;
import java.util.Calendar; import java.util.Calendar;
import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.auth.DefaultAuthentication;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.game.managers.StaminaManager.StaminaManager; import emu.grasscutter.game.managers.StaminaManager.StaminaManager;
import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.PluginManager;
import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.plugin.api.ServerHook;
import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.scripts.ScriptLoader;
import emu.grasscutter.server.http.HttpServer;
import emu.grasscutter.server.http.dispatch.DispatchHandler;
import emu.grasscutter.server.http.handlers.*;
import emu.grasscutter.server.http.dispatch.RegionHandler;
import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.ConfigContainer;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import org.jline.reader.EndOfFileException; import org.jline.reader.EndOfFileException;
...@@ -26,11 +32,9 @@ import ch.qos.logback.classic.Logger; ...@@ -26,11 +32,9 @@ import ch.qos.logback.classic.Logger;
import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.database.DatabaseManager; import emu.grasscutter.database.DatabaseManager;
import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Language;
import emu.grasscutter.server.dispatch.DispatchServer;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.tools.Tools; import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Crypto;
import emu.grasscutter.BuildConfig;
import javax.annotation.Nullable; import javax.annotation.Nullable;
...@@ -48,9 +52,10 @@ public final class Grasscutter { ...@@ -48,9 +52,10 @@ public final class Grasscutter {
private static int day; // Current day of week. private static int day; // Current day of week.
private static DispatchServer dispatchServer; private static HttpServer httpServer;
private static GameServer gameServer; private static GameServer gameServer;
private static PluginManager pluginManager; private static PluginManager pluginManager;
private static AuthenticationSystem authenticationSystem;
public static final Reflections reflector = new Reflections("emu.grasscutter"); public static final Reflections reflector = new Reflections("emu.grasscutter");
public static ConfigContainer config; public static ConfigContainer config;
...@@ -103,25 +108,38 @@ public final class Grasscutter { ...@@ -103,25 +108,38 @@ public final class Grasscutter {
// Initialize database. // Initialize database.
DatabaseManager.initialize(); DatabaseManager.initialize();
// Initialize the default authentication system.
authenticationSystem = new DefaultAuthentication();
// Create server instances. // Create server instances.
dispatchServer = new DispatchServer(); httpServer = new HttpServer();
gameServer = new GameServer(); gameServer = new GameServer();
// Create a server hook instance with both servers. // Create a server hook instance with both servers.
new ServerHook(gameServer, dispatchServer); new ServerHook(gameServer, httpServer);
// Create plugin manager instance. // Create plugin manager instance.
pluginManager = new PluginManager(); 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);
httpServer.addRouter(GachaHandler.class);
// TODO: find a better place? // TODO: find a better place?
StaminaManager.initialize(); StaminaManager.initialize();
// Start servers. // Start servers.
var runMode = SERVER.runMode; var runMode = SERVER.runMode;
if (runMode == ServerRunMode.HYBRID) { if (runMode == ServerRunMode.HYBRID) {
dispatchServer.start(); httpServer.start();
gameServer.start(); gameServer.start();
} else if (runMode == ServerRunMode.DISPATCH_ONLY) { } else if (runMode == ServerRunMode.DISPATCH_ONLY) {
dispatchServer.start(); httpServer.start();
} else if (runMode == ServerRunMode.GAME_ONLY) { } else if (runMode == ServerRunMode.GAME_ONLY) {
gameServer.start(); gameServer.start();
} else { } else {
...@@ -149,6 +167,19 @@ public final class Grasscutter { ...@@ -149,6 +167,19 @@ public final class Grasscutter {
pluginManager.disablePlugins(); 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. * Attempts to load the configuration from a file.
*/ */
...@@ -164,18 +195,12 @@ public final class Grasscutter { ...@@ -164,18 +195,12 @@ public final class Grasscutter {
// If the file already exists, we attempt to load it. // If the file already exists, we attempt to load it.
try (FileReader file = new FileReader(configFile)) { try (FileReader file = new FileReader(configFile)) {
config = gson.fromJson(file, ConfigContainer.class); config = gson.fromJson(file, ConfigContainer.class);
} } catch (Exception exception) {
catch (Exception exception) {
getLogger().error("There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); getLogger().error("There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json.");
System.exit(1); System.exit(1);
} }
} }
public static void loadLanguage() {
var locale = config.language.language;
language = Language.getLanguage(Utils.getLanguageCode(locale));
}
/** /**
* Saves the provided server configuration. * Saves the provided server configuration.
* @param config The configuration to save, or null for a new one. * @param config The configuration to save, or null for a new one.
...@@ -192,45 +217,10 @@ public final class Grasscutter { ...@@ -192,45 +217,10 @@ public final class Grasscutter {
} }
} }
public static void startConsole() { /*
// Console should not start in dispatch only mode. * Getters for the various server components.
if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) { */
getLogger().info(translate("messages.dispatch.no_commands_error"));
return;
}
getLogger().info(translate("messages.status.done"));
getLogger().info(translate("messages.status.version", BuildConfig.VERSION, BuildConfig.GIT_HASH));
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);
}
}
}
public static ConfigContainer getConfig() { public static ConfigContainer getConfig() {
return config; return config;
} }
...@@ -275,8 +265,8 @@ public final class Grasscutter { ...@@ -275,8 +265,8 @@ public final class Grasscutter {
return gson; return gson;
} }
public static DispatchServer getDispatchServer() { public static HttpServer getHttpServer() {
return dispatchServer; return httpServer;
} }
public static GameServer getGameServer() { public static GameServer getGameServer() {
...@@ -286,16 +276,74 @@ public final class Grasscutter { ...@@ -286,16 +276,74 @@ public final class Grasscutter {
public static PluginManager getPluginManager() { public static PluginManager getPluginManager() {
return pluginManager; return pluginManager;
} }
public static AuthenticationSystem getAuthenticationSystem() {
return authenticationSystem;
}
public static int getCurrentDayOfWeek() {
return day;
}
/*
* Utility methods.
*/
public static void updateDayOfWeek() { public static void updateDayOfWeek() {
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
day = calendar.get(Calendar.DAY_OF_WEEK); day = calendar.get(Calendar.DAY_OF_WEEK);
} }
public static int getCurrentDayOfWeek() { public static void startConsole() {
return day; // 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 { public enum ServerRunMode {
HYBRID, DISPATCH_ONLY, GAME_ONLY HYBRID, DISPATCH_ONLY, GAME_ONLY
} }
......
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;
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);
/**
* Called by plugins to internally verify a user's identity.
* @param details A unique, one-time token to verify the user.
* @return True if the user is verified, False otherwise.
*/
boolean verifyUser(String details);
/**
* This is the authenticator used for password authentication.
* @return An authenticator.
*/
Authenticator<LoginResultJson> getPasswordAuthenticator();
/**
* This is the authenticator used for token authentication.
* @return An authenticator.
*/
Authenticator<LoginResultJson> getTokenAuthenticator();
/**
* This is the authenticator used for session authentication.
* @return An authenticator.
*/
Authenticator<ComboTokenResJson> 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;
@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();
}
/**
* 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();
}
}
package emu.grasscutter.auth;
import emu.grasscutter.server.http.objects.*;
/**
* Handles username/password authentication from the client.
* @param <T> The response object type. Should be {@link LoginResultJson} or {@link ComboTokenResJson}
*/
public interface Authenticator<T> {
/**
* 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
package emu.grasscutter.auth;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.DefaultAuthenticators.*;
import emu.grasscutter.server.http.objects.ComboTokenResJson;
import emu.grasscutter.server.http.objects.LoginResultJson;
import static emu.grasscutter.utils.Language.translate;
/**
* The default Grasscutter authentication implementation.
* Allows all users to access any account.
*/
public final class DefaultAuthentication implements AuthenticationSystem {
private final Authenticator<LoginResultJson> passwordAuthenticator = new PasswordAuthenticator();
private final Authenticator<LoginResultJson> tokenAuthenticator = new TokenAuthenticator();
private final Authenticator<ComboTokenResJson> sessionKeyAuthenticator = new SessionKeyAuthenticator();
private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication();
@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 boolean verifyUser(String details) {
Grasscutter.getLogger().info(translate("dispatch.authentication.default_unable_to_verify"));
return false;
}
@Override
public Authenticator<LoginResultJson> getPasswordAuthenticator() {
return this.passwordAuthenticator;
}
@Override
public Authenticator<LoginResultJson> getTokenAuthenticator() {
return this.tokenAuthenticator;
}
@Override
public Authenticator<ComboTokenResJson> getSessionKeyAuthenticator() {
return this.sessionKeyAuthenticator;
}
@Override
public ExternalAuthenticator getExternalAuthenticator() {
return this.externalAuthenticator;
}
}
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 and password form.
*/
public static class PasswordAuthenticator implements Authenticator<LoginResultJson> {
@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<LoginResultJson> {
@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<ComboTokenResJson> {
@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;
}
}
/**
* 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.");
}
}
}
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);
}
...@@ -136,13 +136,13 @@ public class GachaBanner { ...@@ -136,13 +136,13 @@ public class GachaBanner {
} }
public GachaInfo toProto(String sessionKey) { public GachaInfo toProto(String sessionKey) {
String record = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" String record = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort)
+ "/gacha?s=" + sessionKey + "&gachaType=" + gachaType; + "/gacha?s=" + sessionKey + "&gachaType=" + gachaType;
String details = "http" + (DISPATCH_INFO.encryption.useInRouting ? "s" : "") + "://" String details = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(DISPATCH_INFO.accessAddress, DISPATCH_INFO.bindAddress) + ":" + lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(DISPATCH_INFO.accessPort, DISPATCH_INFO.bindPort) + lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort)
+ "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType; + "/gacha/details?s=" + sessionKey + "&gachaType=" + gachaType;
// Grasscutter.getLogger().info("record = " + record); // Grasscutter.getLogger().info("record = " + record);
......
...@@ -3,6 +3,8 @@ package emu.grasscutter.plugin; ...@@ -3,6 +3,8 @@ package emu.grasscutter.plugin;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.plugin.api.ServerHook;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
...@@ -19,6 +21,7 @@ public abstract class Plugin { ...@@ -19,6 +21,7 @@ public abstract class Plugin {
private PluginIdentifier identifier; private PluginIdentifier identifier;
private URLClassLoader classLoader; private URLClassLoader classLoader;
private File dataFolder; private File dataFolder;
private Logger logger;
/** /**
* This method is reflected into. * This method is reflected into.
...@@ -35,6 +38,7 @@ public abstract class Plugin { ...@@ -35,6 +38,7 @@ public abstract class Plugin {
this.identifier = identifier; this.identifier = identifier;
this.classLoader = classLoader; this.classLoader = classLoader;
this.dataFolder = new File(PLUGINS_FOLDER, identifier.name); this.dataFolder = new File(PLUGINS_FOLDER, identifier.name);
this.logger = LoggerFactory.getLogger(identifier.name);
if(!this.dataFolder.exists() && !this.dataFolder.mkdirs()) { if(!this.dataFolder.exists() && !this.dataFolder.mkdirs()) {
Grasscutter.getLogger().warn("Failed to create plugin data folder for " + this.identifier.name); Grasscutter.getLogger().warn("Failed to create plugin data folder for " + this.identifier.name);
...@@ -103,6 +107,14 @@ public abstract class Plugin { ...@@ -103,6 +107,14 @@ public abstract class Plugin {
public final ServerHook getHandle() { public final ServerHook getHandle() {
return this.server; 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. */ /* Called when the plugin is first loaded. */
public void onLoad() { } public void onLoad() { }
......
package emu.grasscutter.plugin.api; package emu.grasscutter.plugin.api;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.command.Command; import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler; import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.dispatch.DispatchServer;
import emu.grasscutter.server.game.GameServer; 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.LinkedList;
import java.util.List; import java.util.List;
...@@ -15,7 +18,7 @@ import java.util.List; ...@@ -15,7 +18,7 @@ import java.util.List;
public final class ServerHook { public final class ServerHook {
private static ServerHook instance; private static ServerHook instance;
private final GameServer gameServer; private final GameServer gameServer;
private final DispatchServer dispatchServer; private final HttpServer httpServer;
/** /**
* Gets the server hook instance. * Gets the server hook instance.
...@@ -28,11 +31,11 @@ public final class ServerHook { ...@@ -28,11 +31,11 @@ public final class ServerHook {
/** /**
* Hooks into a server. * Hooks into a server.
* @param gameServer The game server to hook into. * @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.gameServer = gameServer;
this.dispatchServer = dispatchServer; this.httpServer = httpServer;
instance = this; instance = this;
} }
...@@ -45,10 +48,10 @@ public final class ServerHook { ...@@ -45,10 +48,10 @@ public final class ServerHook {
} }
/** /**
* @return The dispatch server. * @return The HTTP server.
*/ */
public DispatchServer getDispatchServer() { public HttpServer getHttpServer() {
return this.dispatchServer; return this.httpServer;
} }
/** /**
...@@ -70,4 +73,28 @@ public final class ServerHook { ...@@ -70,4 +73,28 @@ public final class ServerHook {
Command commandData = clazz.getAnnotation(Command.class); Command commandData = clazz.getAnnotation(Command.class);
this.gameServer.getCommandMap().registerCommand(commandData.label(), handler); 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<? extends Router> 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
...@@ -70,16 +70,14 @@ public class LuaSerializer implements Serializer { ...@@ -70,16 +70,14 @@ public class LuaSerializer implements Serializer {
} }
try { try {
//noinspection ConfusingArgumentToVarargsMethod
object = type.getDeclaredConstructor().newInstance(null); object = type.getDeclaredConstructor().newInstance(null);
LuaValue[] keys = table.keys(); LuaValue[] keys = table.keys();
for (LuaValue k : keys) { for (LuaValue k : keys) {
try { try {
Field field = object.getClass().getDeclaredField(k.checkjstring()); Field field = object.getClass().getDeclaredField(k.checkjstring());
if (field == null) {
continue;
}
field.setAccessible(true); field.setAccessible(true);
LuaValue keyValue = table.get(k); LuaValue keyValue = table.get(k);
......
package emu.grasscutter.server.dispatch;
import emu.grasscutter.Grasscutter;
import express.http.HttpContextHandler;
import express.http.Request;
import express.http.Response;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
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 {
if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) {
String data = readToString(Paths.get(DATA("GameAnnouncement.json")));
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + data + "}");
} else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) {
String data = readToString(Paths.get(DATA("GameAnnouncementList.json")))
.replace("System.currentTimeMillis()", String.valueOf(System.currentTimeMillis()));
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}");
}
}
private static String readToString(Path path) {
String content = "";
try {
content = Files.readString(path, StandardCharsets.UTF_8);
} catch (IOException ignored) {
Grasscutter.getLogger().warn("Unable to open file " + path.toAbsolutePath());
}
return content;
}
}
\ No newline at end of file
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}");
}
}
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);
/**
* Other plugins may need to verify a user's identity using details from handleLogin()
* @param details The user's unique one-time token that needs to be verified
* @return If the verification was successful
*/
boolean verifyUser(String details);
LoginResultJson handleGameLogin(Request req, LoginAccountRequestJson requestData);
}
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 boolean verifyUser(String details) {
Grasscutter.getLogger().info(translate("dispatch.authentication.default_unable_to_verify"));
return false;
}
@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;
}
}
package emu.grasscutter.server.dispatch.http;
import emu.grasscutter.Grasscutter;
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 java.io.File;
import java.io.IOException;
import java.util.Objects;
import static emu.grasscutter.Configuration.DATA;
public class AnnouncementIndexHandler implements HttpContextHandler {
private final String render_template;
private final String render_swjs;
private final String render_vueminjs;
public AnnouncementIndexHandler() {
File template = new File(Utils.toFilePath(DATA("/hk4e/announcement/index.html")));
File swjs = new File(Utils.toFilePath(DATA("/hk4e/announcement/sw.js")));
File vueminjs = new File(Utils.toFilePath(DATA("/hk4e/announcement/vue.min.js")));
this.render_template = template.exists() ? new String(FileUtils.read(template)) : null;
this.render_swjs = swjs.exists() ? new String(FileUtils.read(swjs)) : null;
this.render_vueminjs = vueminjs.exists() ? new String(FileUtils.read(vueminjs)) : null;
}
@Override
public void handle(Request req, Response res) throws IOException {
if (Objects.equals(req.path(), "/sw.js")) {
res.send(render_swjs);
}else if(Objects.equals(req.path(), "/hk4e/announcement/index.html")) {
res.send(render_template);
}else if(Objects.equals(req.path(), "/dora/lib/vue/2.6.11/vue.min.js")){
res.send(render_vueminjs);
}else{
File renderFile = new File(Utils.toFilePath(DATA(req.path())));
if(renderFile.exists()){
String ext = req.path().substring(req.path().lastIndexOf(".") + 1);
switch(ext){
case "css":
res.type("text/css");
res.send(FileUtils.read(renderFile));
break;
case "js":
default:
res.send(FileUtils.read(renderFile));
break;
}
}else{
Grasscutter.getLogger().info( "File not exist: " + req.path());
}
}
}
}
package emu.grasscutter.server.dispatch.http;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.gacha.GachaBanner;
import emu.grasscutter.game.gacha.GachaManager;
import emu.grasscutter.game.gacha.GachaBanner.BannerType;
import emu.grasscutter.game.player.Player;
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.utils.Language.translate;
import static emu.grasscutter.Configuration.*;
public final class GachaDetailsHandler implements HttpContextHandler {
private final String render_template;
public GachaDetailsHandler() {
File template = new File(Utils.toFilePath(DATA("/gacha_details.html")));
this.render_template = template.exists() ? new String(FileUtils.read(template)) : null;
}
@Override
public void handle(Request req, Response res) throws IOException {
String response = this.render_template;
// Get player info (for langauge).
String sessionKey = req.query("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
Player player = Grasscutter.getGameServer().getPlayerByUid(account.getPlayerUid());
// If the template was not loaded, return an error.
if (this.render_template == null) {
res.send(translate(player, "gacha.details.template_missing"));
return;
}
// Add translated title etc. to the page.
response = response.replace("{{TITLE}}", translate(player, "gacha.details.title"));
response = response.replace("{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars"));
response = response.replace("{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars"));
response = response.replace("{{AVAILABLE_THREE_STARS}}", translate(player, "gacha.details.available_three_stars"));
response = response.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
// Get the banner info for the banner we want.
int gachaType = Integer.parseInt(req.query("gachaType"));
GachaManager manager = Grasscutter.getGameServer().getGachaManager();
GachaBanner banner = manager.getGachaBanners().get(gachaType);
// Add 5-star items.
Set<String> fiveStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getRateUpItems5()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems5Pool1()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems5Pool2()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
response = response.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]");
// Add 4-star items.
Set<String> fourStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getRateUpItems4()).forEach(i -> fourStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems4Pool1()).forEach(i -> fourStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems4Pool2()).forEach(i -> fourStarItems.add(Integer.toString(i)));
response = response.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]");
// Add 3-star items.
Set<String> threeStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getFallbackItems3()).forEach(i -> threeStarItems.add(Integer.toString(i)));
response = response.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
// Done.
res.send(response);
}
}
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.");
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment