Commit 3adf0d44 authored by KingRainbow44's avatar KingRainbow44
Browse files

Refactor dispatch (now called HTTP) server (pt. 2)

parent 840f4706
......@@ -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() {
......
......@@ -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"));
}
......
......@@ -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()
......
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<? 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 {
}
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);
......
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
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);
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 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 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.");
}
}
}
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;
}
}
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; // ?
}
}
package emu.grasscutter.server.dispatch.json;
public class LoginAccountRequestJson {
public String account;
public String password;
public boolean is_crypto;
}
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 = "";
}
}
package emu.grasscutter.server.dispatch.json;
public class LoginTokenRequestJson {
public String uid;
public String token;
}
......@@ -162,11 +162,11 @@ public final class HttpServer {
<!DOCTYPE html>
<html>
<head>
<meta charset=\\"utf8\\">
<meta charset="utf8">
</head>
<body>
<img src=\\"https://http.cat/404\\" />
<img src="https://http.cat/404" />
</body>
</html>
""");
......
......@@ -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;
}
}
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) {
......
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)));
}
}
}
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