Merge unstable into development (#2173)

* Remove more scene synchronized

* Fix worktop options not appearing

* Format code [skip actions]

* Fix delay with server tasks

* Format code [skip actions]

* Fully fix fairy clock (#2146)

* Fix scene transition

* fully fix fairy clock

* Re-add call to `Player#updatePlayerGameTime`

* Format code [skip actions]

* Initialize the script loader in `ResourceLoader#loadAll`

* Fix region removal checking

* Format code [skip actions]

* Use Lombok's `EqualsAndHashCode` for comparing scene regions

* Format code [skip actions]

* Move 'invalid gather object' to `trace`

* Add more information to the 'unknown condition handler' message

* Move invalid ability action to trace

* Make `KcpTunnel` public

* Validate the NPC being talked to

* Format code [skip actions]

* NPCs are not spawned server side; change logic to handle it

* Format code [skip actions]

* unload scene when there are no players (#2147)

* unload scene when there are no players

* Update src/main/java/emu/grasscutter/game/world/Scene.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Check if a command should be copied or HTTP should be used

* Lint Code [skip actions]

* Fix character names rendering incorrectly

* Add basic troubleshooting command

* Implement handbook teleporting

also a few formatting changes and sort data by logical sense

* Fix listener `ConcurrentModificationException` issue

* Add color change to `Join the Community!`

* Lint Code [skip actions]

* Make clickable buttons appear clickable

* Remove 'Mechanicus' entities from the list of entities

* Format code [skip actions]

* Fix going back returning a blank screen

* Implement entity spawning

* Add setting level to entity card

* Add support for 'plain text' mode

* Make descriptions of objects scrollable

* Lint Code [skip actions]

* Format code [skip actions]

* Change the way existing hooks work

* Format code [skip actions]

* Upgrade Javalin to 5.5.0 & Fix project warnings

* Upgrade logging libraries

* Fix gacha mappings static file issue

* Add temporary backwards compatability for `ServerHelper`

* Format code [skip actions]

* Remove artifact signatures from VCS

* Fix forge queue data protocol definition

* Run `spotlessApply`

* Format code [skip actions]

* Download data required for building artifacts

* Add call for Facebook logins

* Add the wiki page as a submodule

* Format code [skip actions]

* Update translation (#2150)

* Update translation

* Update translation

* Separate the dispatch and game servers (pt. 1)

gacha is still broken, handbook still needs to be done

* Format code [skip actions]

* Separate the dispatch and game servers (pt. 2)

this commit fixes the gacha page

* Add description for '/troubleshoot'

* Set default avatar talent level to 10

* Separate the dispatch and game servers (pt. 3)

implement handbook across servers!

* Format code [skip actions]

* Update GitHub Actions to use 'download-file' over 'wget'

* Gm handbook lmao (#2149)

* Fix font issue

* Fix avatars

* Fix text overflow in commands

* Fix virtualized lists and items page 😭😭

* magix why 💀

* use hover style in all minicards

* button

* remove console.log

* lint

* Add icons

* magix asked

* Fix overflow padding issue

* Fix achievement text overflow

* remove icons from repo

* Change command icon

* Add the wiki page as a submodule

* total magix moment

* fix text overflow in commands

* Fix discord button

* Make text scale on Minicard

* import icons and font from another source

* Add hover effects to siebar buttons

* move font and readme to submodule repo

* Make data folder a submodule

* import icons and font from data submodule

* Update README.md

* total magix moment

* magix moment v2

* submodule change

* Import `.webp` files

* Resize `HomeButton`

* Fix 'Copy Command' reappearing after changing pages

---------

Co-authored-by: KingRainbow44 <kobedo11@gmail.com>

* Lint Code [skip actions]

* Download data for the build, not for the lint

* format imports

this is really just to see if build handbook works kek

* Implement proper handbook authentication (pt. 1)

* Implement proper handbook authentication (pt. 2)

* Format code [skip actions]

* Add quest data dumping for the handbook

* Change colors to fit _something suitable_

* Format code [skip actions]

* Fix force pushing to branches after linting

* Fix logic of `SetPlayerPropReq`

* Move more group loading to `trace`

* Add handbook IP authentication in hybrid mode

* Fix player level up not displaying on the client properly

* Format code [skip actions]

* Fix game time locking

* Format code [skip actions]

* Update player properties

* Format code [skip actions]

* Move `warn`s for groups to `debug`

* Fix player pausing

* Move more logs to `trace`

* Use `removeItemById` for deleting items via quests

* Clean up logger more

* Pause in-game time when the world is paused

* Format code [skip actions]

* More player property documentation

* Multi-threaded resource loading

* Format code [skip actions]

* Add quest widgets

* Add quests page (basic impl.)

* Add/fix colors

also fix tailwind

* Remove banned packets

client modifications already perform the job of blocking malicious packets from being executed, no point in having this if self-windy is wanted

* Re-add `BeginCameraSceneLookNotify`

* Fix being unable to attack (#2157)

* Add `PlayerOpenChestEvent`

* Add methods to get players from the server

* Add static methods to register an event handler

* Add `PlayerEnterDungeonEvent`

* Remove legacy documentation from `PlayerMoveEvent`

* Add `PlayerChatEvent`

* Add defaults to `Position`

* Clean up `.utils`

* Revert `Multi-threaded resource loading`

* Fix changing target UID when talking to the server

* Lint Code [skip actions]

* Format code [skip actions]

* fix NPC talk triggering main quest in 46101 (#2158)

Make it so that only talks where the param matches the talkId are checked.

* Format code [skip actions]

* Partially fix Chasing Shadows (#2159)

* Partially fix Chasing Shadows

* Go ahead and move it before the return before Magix tells me to.

* Format code [skip actions]

* Bring back period lol (#2160)

* Disable SNI for the HTTPS server

* Add `EntityCreationEvent`

* Add initial startup message

this is so the server appears like its preparing to start

* Format code [skip actions]

* Enable debug mode for plugin loggers if enabled for the primary logger

* Add documentation about `WorldAreaConfigData`

* Make more fields in excels accessible

* Remove deprecated fields from `GetShopRsp`

* Run `spotlessApply` on definitions

* Add `PlayerEnterAreaEvent`

* Optimize event calls

* Fix event invokes

* Format code [skip actions]

* Remove manual autofinish for main quests. (#2162)

* Add world areas to the textmap cache

* Format code [skip actions]

* Don't overdefine variables in extended classes (#2163)

* Add dumper for world areas

* Format code [skip actions]

* instantiate personalLineList (#2165)

* Fix protocol definitions

thank you Nazrin! (+ hiro for raw definitions)

* Fix the background color leaking from the character widget

* Change HTML spacing to 2 spaces

* Implement hiding widgets

* Change scrollbar to a vibrant color

* Add _some_ scaling to the home buttons and its text

* Build the handbook with Gradle

* Fix the 'finer details' with the handbook UI

* Lint Code [skip actions]

* Fix target destination for the Gradle-built handbook

* Implement fetching a player across servers & Add a chainable JsonObject

useful for plugins! might be used in grasscutter eventually

* Fix GitHub actions

* Fix event calling & canceling

* Run `spotlessApply`

* Rename fields (might be wrong)

* Add/update all/more protocol definitions

* Add/update all/more protocol definitions

* Remove outdated packet

* Fix protocol definitions

* Format code [skip actions]

* Implement some lua variables for less console spam (#2172)

* Implement some lua variables for less console spam

* Add GetHostQuestState

This fixes some chapter 3 stuff.

* Format code [skip actions]

* Fix merge import

* Format code [skip actions]

* Fully fix fairy clock for real this time (#2167)

* Fully fix fairy clock For real this time

* Make it so relogging keeps the time lock state.

* Refactor out questLockTime

* Per Hartie, the client packet needs to be changed too

* Update src/main/java/emu/grasscutter/game/world/World.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update src/main/java/emu/grasscutter/server/packet/recv/HandlerClientLockGameTimeNotify.java

* Remove all code not needed to get clock working

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Implement a proper ability system (#2166)

* Apply fix `21dec2fe`

* Apply fix `89d01d5f`

* Apply fix `d900f154`

this one was already implemented; updated to use call from previous commit

* Ability changing commit

TODO: change info to debug

* Remove use of deprecated methods/fields

* Temp commit v2
(Adding LoseHP and some fixes)

* Oopsie

* Probably fix monster battle

* Fix issue with reflecting into fields

* Fix some things

* Fix ability names for 3.6 resources

* Improve logging

---------

Co-authored-by: StartForKiller <jesussanz2003@gmail.com>

* Format code [skip actions]

* Add system for sending messages between servers

* Format some code

* Remove protocol definitions from Spotless

* Default debug to false; enable with `-debug`

* Implement completely useless global value copying

* HACK: Return the avatar which holds the weapon when the weapon is referred to by ID

* Add properties to `AbilityModifier`

* Change the way HTML is served after authentication

* Use thread executors to speed up the database loading process

* Format code [skip actions]

* Add system for setting handbook address and port

* Lint Code [skip actions]

* Format code [skip actions]

* Fix game-related data not saving

* Format code [skip actions]

* Fix handbook server details

* Lint Code [skip actions]

* Format code [skip actions]

* Use the headers provided by a context to get the IP address

should acknowledge #1975

* Format code [skip actions]

* Move more logs to `trace`

* Format code [skip actions]

* more trace

* Fix something and implement weapon entities

* Format code [skip actions]

* Fix `EntityWeapon`

* Remove deprecated API & Fix resource checking

* Fix unnecessary warning for first-time setup

* Implement handbook request limiting

* Format code [skip actions]

* Fix new avatar weapons being null

* Format code [skip actions]

* Fix issue with 35303 being un-completable & Try to fix fulfilled quest conditions being met

* Load activity config on server startup

* Require plugins to specify an API version and match with the server

* Add default open state ignore list

* Format code [skip actions]

* Quick fix for questing, needs more investigation
This would make the questing work again

* Remove existing hack for 35303

* Fix ignored open states from being set

* Format code [skip actions]

* fix the stupidest bug ive ever seen

* Optimize player kicking on server close

* Format code [skip actions]

* Re-add hack to fix 35303

* Update GitHub actions

* Format code [skip actions]

* Potentially fix issues with regions

* Download additional handbook data

* Revert "Potentially fix issues with regions"

This reverts commit 84e3823695.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: scooterboo <lewasite@yahoo.com>
Co-authored-by: Tesutarin <105267106+Tesutarin@users.noreply.github.com>
Co-authored-by: Scald <104459145+Arikatsu@users.noreply.github.com>
Co-authored-by: StartForKiller <jesussanz2003@gmail.com>
This commit is contained in:
Magix
2023-05-31 20:48:16 -07:00
committed by GitHub
parent f46fd372d2
commit 9e5b57a043
3839 changed files with 1841548 additions and 37533 deletions

View File

@@ -4,18 +4,18 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.utils.FileUtils;
import io.javalin.Javalin;
import io.javalin.core.util.JavalinLogger;
import io.javalin.http.ContentType;
import io.javalin.json.JavalinGson;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.utils.lang.Language.translate;
/**
* Manages all HTTP-related classes.
@@ -28,38 +28,60 @@ public final class HttpServer {
* Configures the Javalin application.
*/
public HttpServer() {
// Check if we are in game only mode.
if (Grasscutter.getRunMode() == Grasscutter.ServerRunMode.GAME_ONLY) {
this.javalin = null;
return;
}
this.javalin = Javalin.create(config -> {
// Set the Javalin HTTP server.
config.server(HttpServer::createServer);
config.jetty.server(HttpServer::createServer);
// Configure encryption/HTTPS/SSL.
config.enforceSsl = HTTP_ENCRYPTION.useEncryption;
if (HTTP_ENCRYPTION.useEncryption)
config.plugins.enableSslRedirects();
// Configure HTTP policies.
if (HTTP_POLICIES.cors.enabled) {
var allowedOrigins = HTTP_POLICIES.cors.allowedOrigins;
if (allowedOrigins.length > 0)
config.enableCorsForOrigin(allowedOrigins);
else config.enableCorsForAllOrigins();
config.plugins.enableCors(cors -> cors.add(corsConfig -> {
if (allowedOrigins.length > 0) {
if (Arrays.asList(allowedOrigins).contains("*"))
corsConfig.anyHost();
else corsConfig.allowHost(Arrays.toString(allowedOrigins));
} else corsConfig.anyHost();
}));
}
// Configure debug logging.
if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL)
config.enableDevLogging();
config.plugins.enableDevLogging();
// Set the JSON mapper.
config.jsonMapper(new JavalinGson());
// Static files should be added like this https://javalin.io/documentation#static-files
});
this.javalin.exception(Exception.class, (exception, ctx) -> {
ctx.status(500).result("Internal server error. %s"
.formatted(exception.getMessage()));
Grasscutter.getLogger().debug("Exception thrown: " +
exception.getMessage(), exception);
});
}
/**
* Creates an HTTP(S) server.
*
* @return A server instance.
*/
@SuppressWarnings("resource")
private static Server createServer() {
Server server = new Server();
ServerConnector serverConnector
= new ServerConnector(server);
= new ServerConnector(server);
if (HTTP_ENCRYPTION.useEncryption) {
var sslContextFactory = new SslContextFactory.Server();
@@ -79,6 +101,7 @@ public final class HttpServer {
try {
sslContextFactory.setKeyStorePath(keystoreFile.getPath());
sslContextFactory.setKeyStorePassword("123456");
sslContextFactory.setSniRequired(false);
Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password"));
} catch (Exception exception) {
@@ -97,6 +120,7 @@ public final class HttpServer {
/**
* Returns the handle for the Express application.
*
* @return A Javalin instance.
*/
public Javalin getHandle() {
@@ -105,13 +129,14 @@ public final class HttpServer {
/**
* Initializes the provided class.
*
* @param router The router class.
* @return Method chaining.
*/
@SuppressWarnings("UnusedReturnValue")
public HttpServer addRouter(Class<? extends Router> router, Object... args) {
// Get all constructor parameters.
Class<?>[] types = new Class<?>[args.length];
var types = new Class<?>[args.length];
for (var argument : args)
types[args.length - 1] = argument.getClass();
@@ -121,18 +146,20 @@ public final class HttpServer {
routerInstance.applyRoutes(this.javalin); // Apply routes.
} catch (Exception exception) {
Grasscutter.getLogger().warn(translate("messages.dispatch.router_error"), exception);
} return this;
}
return this;
}
/**
* Starts listening on the HTTP server.
*
* @throws UnsupportedEncodingException
*/
public void start() throws UnsupportedEncodingException {
// Attempt to start the HTTP server.
if (HTTP_INFO.bindAddress.equals("")) {
this.javalin.start(HTTP_INFO.bindPort);
}else {
} else {
this.javalin.start(HTTP_INFO.bindAddress, HTTP_INFO.bindPort);
}
@@ -144,21 +171,22 @@ public final class HttpServer {
* Handles the '/' (index) endpoint on the Express application.
*/
public static class DefaultRequestRouter implements Router {
@Override public void applyRoutes(Javalin javalin) {
@Override
public void applyRoutes(Javalin javalin) {
javalin.get("/", ctx -> {
// Send file
File file = new File(HTTP_STATIC_FILES.indexFile);
if (!file.exists()) {
ctx.contentType(ContentType.TEXT_HTML);
ctx.result("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<body>%s</body>
</html>
""".formatted(translate("messages.status.welcome")));
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<body>%s</body>
</html>
""".formatted(translate("messages.status.welcome")));
} else {
var filePath = file.getPath();
ContentType fromExtension = ContentType.getContentTypeByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));
@@ -173,7 +201,8 @@ public final class HttpServer {
* Handles unhandled endpoints on the Express application.
*/
public static class UnhandledRequestRouter implements Router {
@Override public void applyRoutes(Javalin javalin) {
@Override
public void applyRoutes(Javalin javalin) {
javalin.error(404, ctx -> {
// Error log
if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING)
@@ -183,17 +212,17 @@ public final class HttpServer {
if (!file.exists()) {
ctx.contentType(ContentType.TEXT_HTML);
ctx.result("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
</head>
<body>
<img src="https://http.cat/404" />
</body>
</html>
""");
<body>
<img src="https://http.cat/404" />
</body>
</html>
""");
} else {
var filePath = file.getPath();
ContentType fromExtension = ContentType.getContentTypeByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));

View File

@@ -3,25 +3,25 @@ package emu.grasscutter.server.http;
import io.javalin.Javalin;
import io.javalin.http.Handler;
/**
* Defines routes for an {@link Javalin} instance.
*/
/** Defines routes for an {@link Javalin} instance. */
public interface Router {
/**
* Called when the router is initialized by Express.
*
* @param javalin A Javalin instance.
*/
void applyRoutes(Javalin javalin);
/**
* Applies this handler to all endpoint types
*
* @param javalin A Javalin instance.
* @param path
* @param ctx
* @return The Javalin instance.
*/
public default Javalin allRoutes(Javalin javalin, String path, Handler ctx) {
default Javalin allRoutes(Javalin javalin, String path, Handler ctx) {
javalin.get(path, ctx);
javalin.post(path, ctx);
javalin.put(path, ctx);

View File

@@ -0,0 +1,172 @@
package emu.grasscutter.server.http.dispatch;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.auth.OAuthAuthenticator.ClientType;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.ComboTokenReqJson;
import emu.grasscutter.server.http.objects.ComboTokenReqJson.LoginTokenData;
import emu.grasscutter.server.http.objects.LoginAccountRequestJson;
import emu.grasscutter.server.http.objects.LoginTokenRequestJson;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Utils;
import io.javalin.Javalin;
import io.javalin.http.Context;
/** Handles requests related to authentication. */
public final class AuthenticationHandler implements Router {
/**
* @route /hk4e_global/mdk/shield/api/login
*/
private static void clientLogin(Context ctx) {
// Parse body data.
String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, LoginAccountRequestJson.class);
// Validate body data.
if (bodyData == null) return;
// Pass data to authentication handler.
var responseData =
Grasscutter.getAuthenticationSystem()
.getPasswordAuthenticator()
.authenticate(AuthenticationSystem.fromPasswordRequest(ctx, bodyData));
// Send response.
ctx.json(responseData);
// Log to console.
Grasscutter.getLogger()
.info(translate("messages.dispatch.account.login_attempt", Utils.address(ctx)));
}
/**
* @route /hk4e_global/mdk/shield/api/verify
*/
private static void tokenLogin(Context ctx) {
// Parse body data.
String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, LoginTokenRequestJson.class);
// Validate body data.
if (bodyData == null) return;
// Pass data to authentication handler.
var responseData =
Grasscutter.getAuthenticationSystem()
.getTokenAuthenticator()
.authenticate(AuthenticationSystem.fromTokenRequest(ctx, bodyData));
// Send response.
ctx.json(responseData);
// Log to console.
Grasscutter.getLogger()
.info(translate("messages.dispatch.account.login_attempt", Utils.address(ctx)));
}
/**
* @route /hk4e_global/combo/granter/login/v2/login
*/
private static void sessionKeyLogin(Context ctx) {
// Parse body data.
String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, ComboTokenReqJson.class);
// Validate body data.
if (bodyData == null || bodyData.data == null) return;
// Decode additional body data.
var tokenData = JsonUtils.decode(bodyData.data, LoginTokenData.class);
// Pass data to authentication handler.
var responseData =
Grasscutter.getAuthenticationSystem()
.getSessionKeyAuthenticator()
.authenticate(AuthenticationSystem.fromComboTokenRequest(ctx, bodyData, tokenData));
// Send response.
ctx.json(responseData);
// Log to console.
Grasscutter.getLogger()
.info(translate("messages.dispatch.account.login_attempt", Utils.address(ctx)));
}
@Override
public void applyRoutes(Javalin javalin) {
// OS
// Username & Password login (from client).
javalin.post("/hk4e_global/mdk/shield/api/login", AuthenticationHandler::clientLogin);
// Cached token login (from registry).
javalin.post("/hk4e_global/mdk/shield/api/verify", AuthenticationHandler::tokenLogin);
// Combo token login (from session key).
javalin.post(
"/hk4e_global/combo/granter/login/v2/login", AuthenticationHandler::sessionKeyLogin);
// CN
// Username & Password login (from client).
javalin.post("/hk4e_cn/mdk/shield/api/login", AuthenticationHandler::clientLogin);
// Cached token login (from registry).
javalin.post("/hk4e_cn/mdk/shield/api/verify", AuthenticationHandler::tokenLogin);
// Combo token login (from session key).
javalin.post("/hk4e_cn/combo/granter/login/v2/login", AuthenticationHandler::sessionKeyLogin);
// External login (from other clients).
javalin.get(
"/authentication/type",
ctx -> ctx.result(Grasscutter.getAuthenticationSystem().getClass().getSimpleName()));
javalin.post(
"/authentication/login",
ctx ->
Grasscutter.getAuthenticationSystem()
.getExternalAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.post(
"/authentication/register",
ctx ->
Grasscutter.getAuthenticationSystem()
.getExternalAuthenticator()
.handleAccountCreation(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.post(
"/authentication/change_password",
ctx ->
Grasscutter.getAuthenticationSystem()
.getExternalAuthenticator()
.handlePasswordReset(AuthenticationSystem.fromExternalRequest(ctx)));
// External login (from OAuth2).
javalin.post(
"/hk4e_global/mdk/shield/api/loginByThirdparty",
ctx ->
Grasscutter.getAuthenticationSystem()
.getOAuthAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.get(
"/authentication/openid/redirect",
ctx ->
Grasscutter.getAuthenticationSystem()
.getOAuthAuthenticator()
.handleTokenProcess(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.get(
"/sdkFacebookLogin.html",
ctx ->
Grasscutter.getAuthenticationSystem()
.getOAuthAuthenticator()
.handleRedirection(
AuthenticationSystem.fromExternalRequest(ctx), ClientType.DESKTOP));
javalin.get(
"/Api/twitter_login",
ctx ->
Grasscutter.getAuthenticationSystem()
.getOAuthAuthenticator()
.handleRedirection(
AuthenticationSystem.fromExternalRequest(ctx), ClientType.DESKTOP));
javalin.get(
"/sdkTwitterLogin.html",
ctx ->
Grasscutter.getAuthenticationSystem()
.getOAuthAuthenticator()
.handleRedirection(
AuthenticationSystem.fromExternalRequest(ctx), ClientType.MOBILE));
}
}

View File

@@ -1,127 +0,0 @@
package emu.grasscutter.server.http.dispatch;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.auth.OAuthAuthenticator.ClientType;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.*;
import emu.grasscutter.server.http.objects.ComboTokenReqJson.LoginTokenData;
import emu.grasscutter.utils.JsonUtils;
import io.javalin.Javalin;
import io.javalin.http.Context;
import static emu.grasscutter.utils.Language.translate;
/**
* Handles requests related to authentication. (aka dispatch)
*/
public final class DispatchHandler implements Router {
@Override public void applyRoutes(Javalin javalin) {
// OS
// Username & Password login (from client).
javalin.post("/hk4e_global/mdk/shield/api/login", DispatchHandler::clientLogin);
// Cached token login (from registry).
javalin.post("/hk4e_global/mdk/shield/api/verify", DispatchHandler::tokenLogin);
// Combo token login (from session key).
javalin.post("/hk4e_global/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin);
// CN
// Username & Password login (from client).
javalin.post("/hk4e_cn/mdk/shield/api/login", DispatchHandler::clientLogin);
// Cached token login (from registry).
javalin.post("/hk4e_cn/mdk/shield/api/verify", DispatchHandler::tokenLogin);
// Combo token login (from session key).
javalin.post("/hk4e_cn/combo/granter/login/v2/login", DispatchHandler::sessionKeyLogin);
// External login (from other clients).
javalin.get("/authentication/type", ctx -> ctx.result(Grasscutter.getAuthenticationSystem().getClass().getSimpleName()));
javalin.post("/authentication/login", ctx -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.post("/authentication/register", ctx -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handleAccountCreation(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.post("/authentication/change_password", ctx -> Grasscutter.getAuthenticationSystem().getExternalAuthenticator()
.handlePasswordReset(AuthenticationSystem.fromExternalRequest(ctx)));
// External login (from OAuth2).
javalin.post("/hk4e_global/mdk/shield/api/loginByThirdparty", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleLogin(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.get("/authentication/openid/redirect", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleTokenProcess(AuthenticationSystem.fromExternalRequest(ctx)));
javalin.get("/Api/twitter_login", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleRedirection(AuthenticationSystem.fromExternalRequest(ctx), ClientType.DESKTOP));
javalin.get("/sdkTwitterLogin.html", ctx -> Grasscutter.getAuthenticationSystem().getOAuthAuthenticator()
.handleRedirection(AuthenticationSystem.fromExternalRequest(ctx), ClientType.MOBILE));
}
/**
* @route /hk4e_global/mdk/shield/api/login
*/
private static void clientLogin(Context ctx) {
// Parse body data.
String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, LoginAccountRequestJson.class);
// Validate body data.
if (bodyData == null)
return;
// Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem()
.getPasswordAuthenticator()
.authenticate(AuthenticationSystem.fromPasswordRequest(ctx, bodyData));
// Send response.
ctx.json(responseData);
// Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", ctx.ip()));
}
/**
* @route /hk4e_global/mdk/shield/api/verify
*/
private static void tokenLogin(Context ctx) {
// Parse body data.
String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, LoginTokenRequestJson.class);
// Validate body data.
if (bodyData == null)
return;
// Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem()
.getTokenAuthenticator()
.authenticate(AuthenticationSystem.fromTokenRequest(ctx, bodyData));
// Send response.
ctx.json(responseData);
// Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", ctx.ip()));
}
/**
* @route /hk4e_global/combo/granter/login/v2/login
*/
private static void sessionKeyLogin(Context ctx) {
// Parse body data.
String rawBodyData = ctx.body();
var bodyData = JsonUtils.decode(rawBodyData, ComboTokenReqJson.class);
// Validate body data.
if (bodyData == null || bodyData.data == null)
return;
// Decode additional body data.
var tokenData = JsonUtils.decode(bodyData.data, LoginTokenData.class);
// Pass data to authentication handler.
var responseData = Grasscutter.getAuthenticationSystem()
.getSessionKeyAuthenticator()
.authenticate(AuthenticationSystem.fromComboTokenRequest(ctx, bodyData, tokenData));
// Send response.
ctx.json(responseData);
// Log to console.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_attempt", ctx.ip()));
}
}

View File

@@ -1,13 +1,17 @@
package emu.grasscutter.server.http.dispatch;
import static emu.grasscutter.config.Configuration.*;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.protobuf.ByteString;
import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp;
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp;
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.net.proto.StopServerInfoOuterClass.StopServerInfo;
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
@@ -15,30 +19,22 @@ import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.QueryCurRegionRspJson;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Utils;
import io.javalin.Javalin;
import io.javalin.http.Context;
import java.time.Instant;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.security.Signature;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.Language.translate;
/**
* Handles requests related to region queries.
*/
/** Handles requests related to region queries. */
public final class RegionHandler implements Router {
private static final Map<String, RegionData> regions = new ConcurrentHashMap<>();
private static String regionListResponse;
private static String regionListResponsecn;
private static String regionListResponseCN;
public RegionHandler() {
try { // Read & initialize region data.
@@ -48,78 +44,116 @@ public final class RegionHandler implements Router {
}
}
/**
* Configures region data according to configuration.
*/
/** Configures region data according to configuration. */
private void initialize() {
String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
var dispatchDomain =
"http"
+ (HTTP_ENCRYPTION.useInRouting ? "s" : "")
+ "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress)
+ ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
// Create regions.
List<RegionSimpleInfo> servers = new ArrayList<>();
List<String> usedNames = new ArrayList<>(); // List to check for potential naming conflicts.
var servers = new ArrayList<RegionSimpleInfo>();
var usedNames = new ArrayList<String>(); // 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.");
var configuredRegions = new ArrayList<>(DISPATCH_INFO.regions);
if (Grasscutter.getRunMode() != ServerRunMode.HYBRID && configuredRegions.size() == 0) {
Grasscutter.getLogger()
.error(
"[Dispatch] There are no game servers available. Exiting due to unplayable state.");
System.exit(1);
} else if (configuredRegions.size() == 0)
configuredRegions.add(new Region("os_usa", DISPATCH_INFO.defaultName,
lr(GAME_INFO.accessAddress, GAME_INFO.bindAddress),
lr(GAME_INFO.accessPort, GAME_INFO.bindPort)));
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;
}
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(dispatchDomain + "/query_cur_region/" + region.Name)
.build();
usedNames.add(region.Name); servers.add(identifier);
// Create a region identifier.
var identifier =
RegionSimpleInfo.newBuilder()
.setName(region.Name)
.setTitle(region.Title)
.setType("DEV_PUBLIC")
.setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name)
.build();
usedNames.add(region.Name);
servers.add(identifier);
// Create a region info object.
var regionInfo = RegionInfo.newBuilder()
.setGateserverIp(region.Ip).setGateserverPort(region.Port)
.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.build();
// Create an updated region query.
var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build();
regions.put(region.Name, new RegionData(updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray())));
});
// Create a region info object.
var regionInfo =
RegionInfo.newBuilder()
.setGateserverIp(region.Ip)
.setGateserverPort(region.Port)
.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.build();
// Create an updated region query.
var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build();
regions.put(
region.Name,
new RegionData(
updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray())));
});
// Determine config settings.
var hiddenIcons = new JsonArray();
hiddenIcons.add(40);
var showExceptions = GameConstants.DEBUG;
// Create a config object.
byte[] customConfig = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes();
Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key.
var customConfig = new JsonObject();
customConfig.addProperty("sdkenv", "2");
customConfig.addProperty("checkdevice", "false");
customConfig.addProperty("loadPatch", "false");
customConfig.addProperty("showexception", String.valueOf(showExceptions));
customConfig.addProperty("regionConfig", "pm|fk|add");
customConfig.addProperty("downloadMode", "0");
customConfig.add("coverSwitch", hiddenIcons);
// XOR the config with the key.
var encodedConfig = JsonUtils.encode(customConfig).getBytes();
Crypto.xor(encodedConfig, Crypto.DISPATCH_KEY);
// Create an updated region list.
QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder()
.addAllRegionList(servers)
.setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig))
.setEnableLoginPc(true).build();
var updatedRegionList =
QueryRegionListHttpRsp.newBuilder()
.addAllRegionList(servers)
.setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.setClientCustomConfigEncrypted(ByteString.copyFrom(encodedConfig))
.setEnableLoginPc(true)
.build();
// Set the region list response.
regionListResponse = Utils.base64Encode(updatedRegionList.toByteString().toByteArray());
// CN
// Create a config object.
byte[] customConfigcn = "{\"sdkenv\":\"0\",\"checkdevice\":\"true\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes();
Crypto.xor(customConfigcn, Crypto.DISPATCH_KEY); // XOR the config with the key.
// Modify the existing config option.
customConfig.addProperty("sdkenv", "0");
// XOR the config with the key.
encodedConfig = JsonUtils.encode(customConfig).getBytes();
Crypto.xor(encodedConfig, Crypto.DISPATCH_KEY);
// Create an updated region list.
QueryRegionListHttpRsp updatedRegionListcn = QueryRegionListHttpRsp.newBuilder()
.addAllRegionList(servers)
.setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.setClientCustomConfigEncrypted(ByteString.copyFrom(customConfigcn))
.setEnableLoginPc(true).build();
var updatedRegionListCN =
QueryRegionListHttpRsp.newBuilder()
.addAllRegionList(servers)
.setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.setClientCustomConfigEncrypted(ByteString.copyFrom(encodedConfig))
.setEnableLoginPc(true)
.build();
// Set the region list response.
regionListResponsecn = Utils.base64Encode(updatedRegionListcn.toByteString().toByteArray());
regionListResponseCN = Utils.base64Encode(updatedRegionListCN.toByteString().toByteArray());
}
@Override
@@ -143,21 +177,21 @@ public final class RegionHandler implements Router {
String platformName = ctx.queryParam("platform");
// Determine the region list to use based on the version and platform.
if ("CNRELiOS".equals(versionCode) || "CNRELWin".equals(versionCode)
if ("CNRELiOS".equals(versionCode)
|| "CNRELWin".equals(versionCode)
|| "CNRELAndroid".equals(versionCode)) {
// Use the CN region list.
QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponsecn);
QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponseCN);
event.call();
logger.debug("Connect to Chinese version");
// Respond with the event result.
ctx.result(event.getRegionList());
} else if ("OSRELiOS".equals(versionCode) || "OSRELWin".equals(versionCode)
} else if ("OSRELiOS".equals(versionCode)
|| "OSRELWin".equals(versionCode)
|| "OSRELAndroid".equals(versionCode)) {
// Use the OS region list.
QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse);
event.call();
logger.debug("Connect to global version");
// Respond with the event result.
ctx.result(event.getRegionList());
@@ -172,7 +206,6 @@ public final class RegionHandler implements Router {
// Use the default region list.
QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse);
event.call();
logger.debug("Connect to global version");
// Respond with the event result.
ctx.result(event.getRegionList());
@@ -181,13 +214,13 @@ public final class RegionHandler implements Router {
// Use the default region list.
QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListResponse);
event.call();
logger.debug("Connect to global version");
// Respond with the event result.
ctx.result(event.getRegionList());
}
// Log the request to the console.
Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", ctx.ip()));
Grasscutter.getLogger()
.info(String.format("[Dispatch] Client %s request: query_region_list", Utils.address(ctx)));
}
/**
@@ -202,39 +235,53 @@ public final class RegionHandler implements Router {
// Get region data.
String regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw==";
if (ctx.queryParamMap().values().size() > 0) {
if (region != null)
regionData = region.getBase64();
if (region != null) regionData = region.getBase64();
}
String clientVersion = versionName.replaceAll(Pattern.compile("[a-zA-Z]").pattern(), "");
String[] versionCode = clientVersion.split("\\.");
int versionMajor = Integer.parseInt(versionCode[0]);
int versionMinor = Integer.parseInt(versionCode[1]);
int versionFix = Integer.parseInt(versionCode[2]);
int versionFix = Integer.parseInt(versionCode[2]);
if (versionMajor >= 3 || (versionMajor == 2 && versionMinor == 7 && versionFix >= 50) || (versionMajor == 2 && versionMinor == 8)) {
if (versionMajor >= 3
|| (versionMajor == 2 && versionMinor == 7 && versionFix >= 50)
|| (versionMajor == 2 && versionMinor == 8)) {
try {
QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call();
QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData);
event.call();
String key_id = ctx.queryParam("key_id");
if (!clientVersion.equals(GameConstants.VERSION)) { // Reject clients when there is a version mismatch
if (!clientVersion.equals(
GameConstants.VERSION)) { // Reject clients when there is a version mismatch
boolean updateClient = GameConstants.VERSION.compareTo(clientVersion) > 0;
QueryCurrRegionHttpRsp rsp = QueryCurrRegionHttpRsp.newBuilder()
.setRetcode(Retcode.RET_STOP_SERVER_VALUE)
.setMsg("Connection Failed!")
.setRegionInfo(RegionInfo.newBuilder())
.setStopServer(StopServerInfo.newBuilder()
.setUrl("https://discord.gg/grasscutters")
.setStopBeginTime((int) Instant.now().getEpochSecond())
.setStopEndTime((int) Instant.now().getEpochSecond()*2)
.setContentMsg(updateClient ? "\nVersion mismatch outdated client! \n\nServer version: %s\nClient version: %s".formatted(GameConstants.VERSION, clientVersion) : "\nVersion mismatch outdated server! \n\nServer version: %s\nClient version: %s".formatted(GameConstants.VERSION, clientVersion))
.build())
.buildPartial();
QueryCurrRegionHttpRsp rsp =
QueryCurrRegionHttpRsp.newBuilder()
.setRetcode(Retcode.RET_STOP_SERVER_VALUE)
.setMsg("Connection Failed!")
.setRegionInfo(RegionInfo.newBuilder())
.setStopServer(
StopServerInfo.newBuilder()
.setUrl("https://discord.gg/grasscutters")
.setStopBeginTime((int) Instant.now().getEpochSecond())
.setStopEndTime((int) Instant.now().getEpochSecond() * 2)
.setContentMsg(
updateClient
? "\nVersion mismatch outdated client! \n\nServer version: %s\nClient version: %s"
.formatted(GameConstants.VERSION, clientVersion)
: "\nVersion mismatch outdated server! \n\nServer version: %s\nClient version: %s"
.formatted(GameConstants.VERSION, clientVersion))
.build())
.buildPartial();
Grasscutter.getLogger().info(String.format("Connection denied for %s due to %s", ctx.ip(), updateClient ? "outdated client!" : "outdated server!"));
Grasscutter.getLogger()
.info(
String.format(
"Connection denied for %s due to %s.",
Utils.address(ctx), updateClient ? "outdated client!" : "outdated server!"));
ctx.json(Crypto.encryptAndSignRegionData(rsp.toByteArray(), key_id));
return;
@@ -251,28 +298,27 @@ public final class RegionHandler implements Router {
return;
}
var regionInfo = Utils.base64Decode(event.getRegionInfo());
ctx.json(Crypto.encryptAndSignRegionData(regionInfo, key_id));
}
catch (Exception e) {
} catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while handling query_cur_region.", e);
}
}
else {
} else {
// Invoke event.
QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData); event.call();
QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(regionData);
event.call();
// Respond with event result.
ctx.result(event.getRegionInfo());
}
// Log to console.
Grasscutter.getLogger().info(String.format("Client %s request: query_cur_region/%s", ctx.ip(), regionName));
Grasscutter.getLogger()
.info(
String.format(
"Client %s request: query_cur_region/%s", Utils.address(ctx), regionName));
}
/**
* Region data container.
*/
/** Region data container. */
public static class RegionData {
private final QueryCurrRegionHttpRsp regionQuery;
private final String base64;
@@ -293,9 +339,12 @@ public final class RegionHandler implements Router {
/**
* 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;
return Grasscutter.getRunMode() == ServerRunMode.HYBRID
? regions.get("os_usa").getRegionQuery()
: null;
}
}

View File

@@ -1,20 +1,23 @@
package emu.grasscutter.server.http.documentation;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.Router;
import io.javalin.Javalin;
import io.javalin.http.Context;
public final class DocumentationServerHandler implements Router {
@Override
public void applyRoutes(Javalin javalin) {
final RootRequestHandler root = new RootRequestHandler();
final HandbookRequestHandler handbook = new HandbookRequestHandler();
final GachaMappingRequestHandler gachaMapping = new GachaMappingRequestHandler();
final var root = new RootRequestHandler();
final var gachaMapping = new GachaMappingRequestHandler();
// TODO: Removal
// TODO: Forward /documentation requests to https://grasscutter.io/wiki
javalin.get("/documentation/handbook", handbook::handle);
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.DISPATCH_ONLY) {
final var handbook = new HandbookRequestHandler();
javalin.get("/documentation/handbook", handbook::handle);
}
javalin.get("/documentation/gachamapping", gachaMapping::handle);
javalin.get("/documentation", root::handle);
}

View File

@@ -1,16 +1,15 @@
package emu.grasscutter.server.http.documentation;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.Language;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import static emu.grasscutter.config.Configuration.DOCUMENT_LANGUAGE;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.lang.Language;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import java.util.List;
final class GachaMappingRequestHandler implements DocumentationHandler {
private List<String> gachaJsons;
private final List<String> gachaJsons;
GachaMappingRequestHandler() {
this.gachaJsons = Tools.createGachaMappingJsons();
@@ -18,7 +17,10 @@ final class GachaMappingRequestHandler implements DocumentationHandler {
@Override
public void handle(Context ctx) {
final int langIdx = Language.TextStrings.MAP_LANGUAGES.getOrDefault(DOCUMENT_LANGUAGE, 0); // TODO: This should really be based off the client language somehow
final int langIdx =
Language.TextStrings.MAP_LANGUAGES.getOrDefault(
DOCUMENT_LANGUAGE,
0); // TODO: This should really be based off the client language somehow
ctx.contentType(ContentType.APPLICATION_JSON).result(gachaJsons.get(langIdx));
}
}

View File

@@ -0,0 +1,263 @@
package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.config.Configuration.HANDBOOK;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.*;
import emu.grasscutter.utils.objects.*;
import emu.grasscutter.utils.objects.HandbookBody.Action;
import io.javalin.Javalin;
import io.javalin.http.*;
import java.util.*;
import java.util.concurrent.*;
/** Handles requests for the new GM Handbook. */
public final class HandbookHandler implements Router {
private String handbook;
private final boolean serve;
private final Map<String, Integer> currentRequests = new ConcurrentHashMap<>();
/**
* Constructor for the handbook router. Enables serving the handbook if the handbook file is
* found.
*/
public HandbookHandler() {
this.handbook = new String(FileUtils.readResource("/html/handbook.html"));
this.serve = HANDBOOK.enable && this.handbook.length() > 0;
var server = HANDBOOK.server;
if (this.serve && server.enforced) {
this.handbook =
this.handbook
.replace("{{DETAILS_ADDRESS}}", server.address)
.replace("{{DETAILS_PORT}}", String.valueOf(server.port))
.replace("{{DETAILS_DISABLE}}", Boolean.toString(!server.canChange));
}
// Create a new task to reset the request count.
if (HANDBOOK.limits.enabled) {
new Timer()
.scheduleAtFixedRate(
new TimerTask() {
@Override
public void run() {
currentRequests.clear();
}
},
0,
TimeUnit.SECONDS.toMillis(HANDBOOK.limits.interval));
}
}
@Override
public void applyRoutes(Javalin javalin) {
if (!this.serve) return;
// The handbook content. (built from src/handbook)
javalin.get("/handbook", this::serveHandbook);
// The handbook authentication page.
javalin.get("/handbook/authenticate", this::authenticate);
javalin.post("/handbook/authenticate", this::performAuthentication);
// Handbook control routes.
javalin.post("/handbook/avatar", this::grantAvatar);
javalin.post("/handbook/item", this::giveItem);
javalin.post("/handbook/teleport", this::teleportTo);
javalin.post("/handbook/spawn", this::spawnEntity);
}
/**
* @return True if the server can execute handbook commands.
*/
private boolean controlSupported() {
return HANDBOOK.enable && HANDBOOK.allowCommands;
}
/**
* Checks the request against the normal request limits.
*
* @param ctx The Javalin request context.
* @return True if the request is within the normal limits.
*/
private boolean normalLimit(Context ctx) {
var limits = HANDBOOK.limits;
if (!limits.enabled) return true;
// Check the request count.
var address = Utils.address(ctx);
var count = this.currentRequests.getOrDefault(address, 0);
if (++count >= limits.maxRequests) {
// Respond to the request.
ctx.status(429).result(JObject.c().add("timestamp", System.currentTimeMillis()).toString());
return false;
}
// Update the request count.
this.currentRequests.put(address, count);
return true;
}
/**
* Serves the handbook if it is found.
*
* @route GET /handbook
* @param ctx The Javalin request context.
*/
private void serveHandbook(Context ctx) {
if (!this.serve) {
ctx.status(500).result("Handbook not found.");
} else {
ctx.contentType(ContentType.TEXT_HTML).result(this.handbook);
}
}
/**
* Serves the handbook authentication page.
*
* @route GET /handbook/authenticate
* @param ctx The Javalin request context.
*/
private void authenticate(Context ctx) {
if (!this.serve) {
ctx.status(500).result("Handbook not found.");
} else {
// Pass the request to the authenticator.
Grasscutter.getAuthenticationSystem()
.getHandbookAuthenticator()
.presentPage(AuthenticationRequest.builder().context(ctx).build());
}
}
/**
* Performs authentication for the handbook.
*
* @route POST /handbook/authenticate
* @param ctx The Javalin request context.
*/
private void performAuthentication(Context ctx) {
if (!this.serve) {
ctx.status(500).result("Handbook not found.");
} else {
// Pass the request to the authenticator.
var result =
Grasscutter.getAuthenticationSystem()
.getHandbookAuthenticator()
.authenticate(AuthenticationRequest.builder().context(ctx).build());
if (result == null) {
ctx.status(500).result("Authentication failed.");
} else {
ctx.status(result.getStatus())
.result(result.getBody())
.contentType(result.isHtml() ? ContentType.TEXT_HTML : ContentType.TEXT_PLAIN);
}
}
}
/**
* Grants the avatar to the user.
*
* @route POST /handbook/avatar
* @param ctx The Javalin request context.
*/
private void grantAvatar(Context ctx) {
if (!this.controlSupported()) {
ctx.status(500).result("Handbook control not supported.");
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.GrantAvatar.class);
// Get the response.
var response = DispatchUtils.performHandbookAction(Action.GRANT_AVATAR, request);
// Send the response.
ctx.status(response.getStatus() > 100 ? response.getStatus() : 500).json(response);
}
/**
* Gives an item to the user.
*
* @route POST /handbook/item
* @param ctx The Javalin request context.
*/
private void giveItem(Context ctx) {
if (!this.controlSupported()) {
ctx.status(500).result("Handbook control not supported.");
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.GiveItem.class);
// Get the response.
var response = DispatchUtils.performHandbookAction(Action.GIVE_ITEM, request);
// Send the response.
ctx.status(response.getStatus() > 100 ? response.getStatus() : 500).json(response);
}
/**
* Teleports the user to a location.
*
* @route POST /handbook/teleport
* @param ctx The Javalin request context.
*/
private void teleportTo(Context ctx) {
if (!this.controlSupported()) {
ctx.status(500).result("Handbook control not supported.");
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.TeleportTo.class);
// Get the response.
var response = DispatchUtils.performHandbookAction(Action.TELEPORT_TO, request);
// Send the response.
ctx.status(response.getStatus() > 100 ? response.getStatus() : 500).json(response);
}
/**
* Spawns an entity in the world.
*
* @route POST /handbook/spawn
* @param ctx The Javalin request context.
*/
private void spawnEntity(Context ctx) {
if (!this.controlSupported()) {
ctx.status(500).result("Handbook control not supported.");
return;
}
// Check for rate limiting.
if (!this.normalLimit(ctx)) return;
// Parse the request body into a class.
var request = ctx.bodyAsClass(HandbookBody.SpawnEntity.class);
// Check the entity limit.
var entityLimit =
HANDBOOK.limits.enabled ? Math.max(HANDBOOK.limits.maxEntities, 0) : Long.MAX_VALUE;
if (request.getAmount() > entityLimit) {
ctx.status(400)
.result(
JObject.c()
.add("timestamp", System.currentTimeMillis())
.add("error", "Entity limit exceeded.")
.toString());
return;
}
// Get the response.
var response = DispatchUtils.performHandbookAction(Action.SPAWN_ENTITY, request);
// Send the response.
ctx.status(response.getStatus() > 100 ? response.getStatus() : 500).json(response);
}
}

View File

@@ -1,16 +1,14 @@
package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.config.Configuration.*;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.AvatarData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.SceneData;
import emu.grasscutter.data.excels.avatar.AvatarData;
import emu.grasscutter.data.excels.monster.MonsterData;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Language;
import emu.grasscutter.utils.lang.Language;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@@ -42,7 +40,7 @@ final class HandbookRequestHandler implements DocumentationHandler {
Matcher matcher = localePattern.matcher(acceptLanguage);
if (matcher.find()) {
String lang = matcher.group(0);
langIdx = Language.TextStrings.MAP_GC_LANGUAGES.getOrDefault(lang,0);
langIdx = Language.TextStrings.MAP_GC_LANGUAGES.getOrDefault(lang, 0);
}
}
@@ -61,88 +59,145 @@ final class HandbookRequestHandler implements DocumentationHandler {
final List<String> output = new ArrayList<>(NUM_LANGUAGES);
final List<Language> languages = Language.TextStrings.getLanguages();
final List<StringBuilder> sbs = new ArrayList<>(NUM_LANGUAGES);
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.add(new StringBuilder(""));
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) sbs.add(new StringBuilder());
// Commands table
CommandMap.getInstance().getHandlersAsList().forEach(cmd -> {
String label = cmd.getLabel();
String descKey = cmd.getDescriptionKey();
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx).append("<tr><td><code>" + label + "</code></td><td>" + languages.get(langIdx).get(descKey) + "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n
CommandMap.getInstance()
.getHandlersAsList()
.forEach(
cmd -> {
String label = cmd.getLabel();
String descKey = cmd.getDescriptionKey();
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx)
.append(
"<tr><td><code>"
+ label
+ "</code></td><td>"
+ languages.get(langIdx).get(descKey)
+ "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length() - 1)); // Remove trailing \n
final List<String> cmdsTable = sbs.stream().map(StringBuilder::toString).toList();
// Avatars table
final Int2ObjectMap<AvatarData> avatarMap = GameData.getAvatarDataMap();
sbs.forEach(sb -> sb.setLength(0));
avatarMap.keySet().intStream().sorted().mapToObj(avatarMap::get).forEach(data -> {
int id = data.getId();
Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + name.get(langIdx) + "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n
avatarMap
.keySet()
.intStream()
.sorted()
.mapToObj(avatarMap::get)
.forEach(
data -> {
int id = data.getId();
Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx)
.append(
"<tr><td><code>"
+ id
+ "</code></td><td>"
+ name.get(langIdx)
+ "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length() - 1)); // Remove trailing \n
final List<String> avatarsTable = sbs.stream().map(StringBuilder::toString).toList();
// Items table
final Int2ObjectMap<ItemData> itemMap = GameData.getItemDataMap();
sbs.forEach(sb -> sb.setLength(0));
itemMap.keySet().intStream().sorted().mapToObj(itemMap::get).forEach(data -> {
int id = data.getId();
Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + name.get(langIdx) + "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n
itemMap
.keySet()
.intStream()
.sorted()
.mapToObj(itemMap::get)
.forEach(
data -> {
int id = data.getId();
Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx)
.append(
"<tr><td><code>"
+ id
+ "</code></td><td>"
+ name.get(langIdx)
+ "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length() - 1)); // Remove trailing \n
final List<String> itemsTable = sbs.stream().map(StringBuilder::toString).toList();
// Scenes table
final Int2ObjectMap<SceneData> sceneMap = GameData.getSceneDataMap();
sceneMap.keySet().intStream().sorted().mapToObj(sceneMap::get).forEach(data -> {
int id = data.getId();
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + data.getScriptData() + "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n
sceneMap
.keySet()
.intStream()
.sorted()
.mapToObj(sceneMap::get)
.forEach(
data -> {
int id = data.getId();
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx)
.append(
"<tr><td><code>"
+ id
+ "</code></td><td>"
+ data.getScriptData()
+ "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length() - 1)); // Remove trailing \n
final List<String> scenesTable = sbs.stream().map(StringBuilder::toString).toList();
// Monsters table
final Int2ObjectMap<MonsterData> monsterMap = GameData.getMonsterDataMap();
monsterMap.keySet().intStream().sorted().mapToObj(monsterMap::get).forEach(data -> {
int id = data.getId();
Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + name.get(langIdx) + "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n
monsterMap
.keySet()
.intStream()
.sorted()
.mapToObj(monsterMap::get)
.forEach(
data -> {
int id = data.getId();
Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
sbs.get(langIdx)
.append(
"<tr><td><code>"
+ id
+ "</code></td><td>"
+ name.get(langIdx)
+ "</td></tr>\n");
});
sbs.forEach(sb -> sb.setLength(sb.length() - 1)); // Remove trailing \n
final List<String> monstersTable = sbs.stream().map(StringBuilder::toString).toList();
// Add translated title etc. to the page.
for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
Language lang = languages.get(langIdx);
output.add(template
.replace("{{TITLE}}", lang.get("documentation.handbook.title"))
.replace("{{TITLE_COMMANDS}}", lang.get("documentation.handbook.title_commands"))
.replace("{{TITLE_AVATARS}}", lang.get("documentation.handbook.title_avatars"))
.replace("{{TITLE_ITEMS}}", lang.get("documentation.handbook.title_items"))
.replace("{{TITLE_SCENES}}", lang.get("documentation.handbook.title_scenes"))
.replace("{{TITLE_MONSTERS}}", lang.get("documentation.handbook.title_monsters"))
.replace("{{HEADER_ID}}", lang.get("documentation.handbook.header_id"))
.replace("{{HEADER_COMMAND}}", lang.get("documentation.handbook.header_command"))
.replace("{{HEADER_DESCRIPTION}}", lang.get("documentation.handbook.header_description"))
.replace("{{HEADER_AVATAR}}", lang.get("documentation.handbook.header_avatar"))
.replace("{{HEADER_ITEM}}", lang.get("documentation.handbook.header_item"))
.replace("{{HEADER_SCENE}}", lang.get("documentation.handbook.header_scene"))
.replace("{{HEADER_MONSTER}}", lang.get("documentation.handbook.header_monster"))
// Commands table
.replace("{{COMMANDS_TABLE}}", cmdsTable.get(langIdx))
.replace("{{AVATARS_TABLE}}", avatarsTable.get(langIdx))
.replace("{{ITEMS_TABLE}}", itemsTable.get(langIdx))
.replace("{{SCENES_TABLE}}", scenesTable.get(langIdx))
.replace("{{MONSTERS_TABLE}}", monstersTable.get(langIdx))
);
output.add(
template
.replace("{{TITLE}}", lang.get("documentation.handbook.title"))
.replace("{{TITLE_COMMANDS}}", lang.get("documentation.handbook.title_commands"))
.replace("{{TITLE_AVATARS}}", lang.get("documentation.handbook.title_avatars"))
.replace("{{TITLE_ITEMS}}", lang.get("documentation.handbook.title_items"))
.replace("{{TITLE_SCENES}}", lang.get("documentation.handbook.title_scenes"))
.replace("{{TITLE_MONSTERS}}", lang.get("documentation.handbook.title_monsters"))
.replace("{{HEADER_ID}}", lang.get("documentation.handbook.header_id"))
.replace("{{HEADER_COMMAND}}", lang.get("documentation.handbook.header_command"))
.replace(
"{{HEADER_DESCRIPTION}}", lang.get("documentation.handbook.header_description"))
.replace("{{HEADER_AVATAR}}", lang.get("documentation.handbook.header_avatar"))
.replace("{{HEADER_ITEM}}", lang.get("documentation.handbook.header_item"))
.replace("{{HEADER_SCENE}}", lang.get("documentation.handbook.header_scene"))
.replace("{{HEADER_MONSTER}}", lang.get("documentation.handbook.header_monster"))
// Commands table
.replace("{{COMMANDS_TABLE}}", cmdsTable.get(langIdx))
.replace("{{AVATARS_TABLE}}", avatarsTable.get(langIdx))
.replace("{{ITEMS_TABLE}}", itemsTable.get(langIdx))
.replace("{{SCENES_TABLE}}", scenesTable.get(langIdx))
.replace("{{MONSTERS_TABLE}}", monstersTable.get(langIdx)));
}
return output;
}

View File

@@ -1,12 +1,11 @@
package emu.grasscutter.server.http.documentation;
import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.utils.FileUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import java.io.IOException;
import java.nio.file.Files;
@@ -32,9 +31,11 @@ final class RootRequestHandler implements DocumentationHandler {
return;
}
String content = template.replace("{{TITLE}}", translate("documentation.index.title"))
.replace("{{ITEM_HANDBOOK}}", translate("documentation.index.handbook"))
.replace("{{ITEM_GACHA_MAPPING}}", translate("documentation.index.gacha_mapping"));
String content =
template
.replace("{{TITLE}}", translate("documentation.index.title"))
.replace("{{ITEM_HANDBOOK}}", translate("documentation.index.handbook"))
.replace("{{ITEM_GACHA_MAPPING}}", translate("documentation.index.gacha_mapping"));
ctx.contentType(ContentType.TEXT_HTML);
ctx.result(content);
}

View File

@@ -1,43 +1,26 @@
package emu.grasscutter.server.http.handlers;
import static emu.grasscutter.config.Configuration.*;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.DataLoader;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.utils.FileUtils;
import io.javalin.Javalin;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import static emu.grasscutter.config.Configuration.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.StringJoiner;
/**
* Handles requests related to the announcements page.
*/
/** Handles requests related to the announcements page. */
public final class AnnouncementsHandler implements Router {
@Override public void applyRoutes(Javalin javalin) {
// hk4e-api-os.hoyoverse.com
this.allRoutes(javalin, "/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}"));
// hk4e-api-os.hoyoverse.com
this.allRoutes(javalin,"/common/hk4e_global/announcement/api/getAlertAnn", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}"));
// hk4e-api-os.hoyoverse.com
this.allRoutes(javalin,"/common/hk4e_global/announcement/api/getAnnList", AnnouncementsHandler::getAnnouncement);
// hk4e-api-os-static.hoyoverse.com
this.allRoutes(javalin,"/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement);
// hk4e-sdk-os.hoyoverse.com
this.allRoutes(javalin,"/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}"));
javalin.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources);
}
private static void getAnnouncement(Context ctx) {
String data = "";
if (Objects.equals(ctx.endpointHandlerPath(), "/common/hk4e_global/announcement/api/getAnnContent")) {
if (Objects.equals(
ctx.endpointHandlerPath(), "/common/hk4e_global/announcement/api/getAnnContent")) {
try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncement.json"));
} catch (Exception e) {
@@ -45,7 +28,8 @@ public final class AnnouncementsHandler implements Router {
Grasscutter.getLogger().info("Unable to read file 'GameAnnouncementList.json'. \n" + e);
}
}
} else if (Objects.equals(ctx.endpointHandlerPath(), "/common/hk4e_global/announcement/api/getAnnList")) {
} else if (Objects.equals(
ctx.endpointHandlerPath(), "/common/hk4e_global/announcement/api/getAnnList")) {
try {
data = FileUtils.readToString(DataLoader.load("GameAnnouncementList.json"));
} catch (Exception e) {
@@ -62,13 +46,17 @@ public final class AnnouncementsHandler implements Router {
return;
}
String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
String dispatchDomain =
"http"
+ (HTTP_ENCRYPTION.useInRouting ? "s" : "")
+ "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress)
+ ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
data = data
.replace("{{DISPATCH_PUBLIC}}", dispatchDomain)
.replace("{{SYSTEM_TIME}}", String.valueOf(System.currentTimeMillis()));
data =
data.replace("{{DISPATCH_PUBLIC}}", dispatchDomain)
.replace("{{SYSTEM_TIME}}", String.valueOf(System.currentTimeMillis()));
ctx.result("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}");
}
@@ -86,7 +74,9 @@ public final class AnnouncementsHandler implements Router {
try (InputStream filestream = DataLoader.load(stringJoiner.toString())) {
String possibleFilename = ctx.path();
ContentType fromExtension = ContentType.getContentTypeByExtension(possibleFilename.substring(possibleFilename.lastIndexOf(".") + 1));
ContentType fromExtension =
ContentType.getContentTypeByExtension(
possibleFilename.substring(possibleFilename.lastIndexOf(".") + 1));
ctx.contentType(fromExtension != null ? fromExtension : ContentType.APPLICATION_OCTET_STREAM);
ctx.result(filestream.readAllBytes());
} catch (Exception e) {
@@ -94,4 +84,38 @@ public final class AnnouncementsHandler implements Router {
ctx.status(404);
}
}
@Override
public void applyRoutes(Javalin javalin) {
// hk4e-api-os.hoyoverse.com
this.allRoutes(
javalin,
"/common/hk4e_global/announcement/api/getAlertPic",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}"));
// hk4e-api-os.hoyoverse.com
this.allRoutes(
javalin,
"/common/hk4e_global/announcement/api/getAlertAnn",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}"));
// hk4e-api-os.hoyoverse.com
this.allRoutes(
javalin,
"/common/hk4e_global/announcement/api/getAnnList",
AnnouncementsHandler::getAnnouncement);
// hk4e-api-os-static.hoyoverse.com
this.allRoutes(
javalin,
"/common/hk4e_global/announcement/api/getAnnContent",
AnnouncementsHandler::getAnnouncement);
// hk4e-sdk-os.hoyoverse.com
this.allRoutes(
javalin,
"/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}"));
javalin.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources);
}
}

View File

@@ -1,91 +1,78 @@
package emu.grasscutter.server.http.handlers;
import static emu.grasscutter.utils.lang.Language.translate;
import com.google.gson.JsonObject;
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.GachaSystem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.DispatchUtils;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import io.javalin.Javalin;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.staticfiles.Location;
import lombok.Getter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import lombok.Getter;
import static emu.grasscutter.utils.Language.translate;
/**
* Handles all gacha-related HTTP requests.
*/
/** Handles all gacha-related HTTP requests. */
public final class GachaHandler implements Router {
@Getter private static final Path gachaMappingsPath = FileUtils.getDataUserPath("gacha/mappings.js");
@Getter
private static final Path gachaMappingsPath = FileUtils.getDataUserPath("gacha/mappings.js");
@Deprecated(forRemoval = true)
public static final String gachaMappings = gachaMappingsPath.toString();
@Override public void applyRoutes(Javalin javalin) {
javalin.get("/gacha", GachaHandler::gachaRecords);
javalin.get("/gacha/details", GachaHandler::gachaDetails);
javalin._conf.addSinglePageRoot("/gacha/mappings", gachaMappingsPath.toString(), Location.EXTERNAL); // TODO: This ***must*** be changed to take the Path not a String. This might involve upgrading Javalin.
}
private static void gachaRecords(Context ctx) {
String sessionKey = ctx.queryParam("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
var sessionKey = ctx.queryParam("s");
var account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if (account == null) {
ctx.status(403).result("Requested account was not found");
return;
}
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
if (player == null) {
ctx.status(403).result("No player associated with requested account");
ctx.status(403).result("Requested account was not found.");
return;
}
// Get page and gacha type.
int page = 0, gachaType = 0;
if (ctx.queryParam("p") != null)
page = Integer.parseInt(ctx.queryParam("p"));
if (ctx.queryParam("gachaType") != null)
gachaType = Integer.parseInt(ctx.queryParam("gachaType"));
String records = DatabaseHelper.getGachaRecords(player.getUid(), page, gachaType).toString();
long maxPage = DatabaseHelper.getGachaRecordsMaxPage(player.getUid(), page, gachaType);
var pageStr = ctx.queryParam("p");
if (pageStr != null) page = Integer.parseInt(pageStr);
String template = new String(FileUtils.read(FileUtils.getDataPath("gacha/records.html")), StandardCharsets.UTF_8)
.replace("{{REPLACE_RECORDS}}", records)
.replace("{{REPLACE_MAXPAGE}}", String.valueOf(maxPage))
.replace("{{TITLE}}", translate(player, "gacha.records.title"))
.replace("{{DATE}}", translate(player, "gacha.records.date"))
.replace("{{ITEM}}", translate(player, "gacha.records.item"))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
var gachaTypeStr = ctx.queryParam("gachaType");
if (gachaTypeStr != null) gachaType = Integer.parseInt(gachaTypeStr);
// Make request to dispatch server.
var data = DispatchUtils.fetchGachaRecords(account.getId(), page, gachaType);
var records = data.get("records").getAsJsonArray();
var maxPage = data.get("maxPage").getAsLong();
var locale = account.getLocale();
var template =
new String(
FileUtils.read(FileUtils.getDataPath("gacha/records.html")), StandardCharsets.UTF_8)
.replace("'{{REPLACE_RECORDS}}'", records.toString())
.replace("'{{REPLACE_MAXPAGE}}'", String.valueOf(maxPage))
.replace("{{TITLE}}", translate(locale, "gacha.records.title"))
.replace("{{DATE}}", translate(locale, "gacha.records.date"))
.replace("{{ITEM}}", translate(locale, "gacha.records.item"))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
ctx.contentType(ContentType.TEXT_HTML);
ctx.result(template);
}
private static void gachaDetails(Context ctx) {
Path detailsTemplate = FileUtils.getDataPath("gacha/details.html");
String sessionKey = ctx.queryParam("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
var detailsTemplate = FileUtils.getDataPath("gacha/details.html");
var sessionKey = ctx.queryParam("s");
var account = DatabaseHelper.getAccountBySessionKey(sessionKey);
if (account == null) {
ctx.status(403).result("Requested account was not found");
return;
}
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
if (player == null) {
ctx.status(403).result("No player associated with requested account");
return;
}
String template;
try {
@@ -97,37 +84,54 @@ public final class GachaHandler implements Router {
}
// Add translated title etc. to the page.
template = template.replace("{{TITLE}}", translate(player, "gacha.details.title"))
.replace("{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars"))
.replace("{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars"))
.replace("{{AVAILABLE_THREE_STARS}}", translate(player, "gacha.details.available_three_stars"))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
var locale = account.getLocale();
template =
template
.replace("{{TITLE}}", translate(locale, "gacha.details.title"))
.replace(
"{{AVAILABLE_FIVE_STARS}}", translate(locale, "gacha.details.available_five_stars"))
.replace(
"{{AVAILABLE_FOUR_STARS}}", translate(locale, "gacha.details.available_four_stars"))
.replace(
"{{AVAILABLE_THREE_STARS}}",
translate(locale, "gacha.details.available_three_stars"))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
// Get the banner info for the banner we want.
int scheduleId = Integer.parseInt(ctx.queryParam("scheduleId"));
GachaSystem manager = Grasscutter.getGameServer().getGachaSystem();
GachaBanner banner = manager.getGachaBanners().get(scheduleId);
var scheduleIdStr = ctx.queryParam("scheduleId");
if (scheduleIdStr == null) {
ctx.status(400).result("Missing scheduleId parameter");
return;
}
var scheduleId = Integer.parseInt(scheduleIdStr);
var manager = Grasscutter.getGameServer().getGachaSystem();
var banner = manager.getGachaBanners().get(scheduleId);
// Add 5-star items.
Set<String> fiveStarItems = new LinkedHashSet<>();
var fiveStarItems = new LinkedHashSet<String>();
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)));
Arrays.stream(banner.getFallbackItems5Pool1())
.forEach(i -> fiveStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems5Pool2())
.forEach(i -> fiveStarItems.add(Integer.toString(i)));
template = template.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]");
// Add 4-star items.
Set<String> fourStarItems = new LinkedHashSet<>();
var fourStarItems = new LinkedHashSet<String>();
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)));
Arrays.stream(banner.getFallbackItems4Pool1())
.forEach(i -> fourStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems4Pool2())
.forEach(i -> fourStarItems.add(Integer.toString(i)));
template = template.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]");
// Add 3-star items.
Set<String> threeStarItems = new LinkedHashSet<>();
var threeStarItems = new LinkedHashSet<String>();
Arrays.stream(banner.getFallbackItems3()).forEach(i -> threeStarItems.add(Integer.toString(i)));
template = template.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
@@ -135,4 +139,30 @@ public final class GachaHandler implements Router {
ctx.contentType(ContentType.TEXT_HTML);
ctx.result(template);
}
/**
* Fetches the gacha records for the specified player.
*
* @param player The player to fetch the records for.
* @param response The response to write to.
* @param page The page to fetch.
* @param type The gacha type to fetch.
*/
public static void fetchGachaRecords(Player player, JsonObject response, int page, int type) {
var playerId = player.getUid();
var records = DatabaseHelper.getGachaRecords(playerId, page, type).toString();
var maxPage = DatabaseHelper.getGachaRecordsMaxPage(playerId, page, type);
// Finish the response.
response.addProperty("retcode", 0);
response.addProperty("records", records);
response.addProperty("maxPage", maxPage);
}
@Override
public void applyRoutes(Javalin javalin) {
javalin.get("/gacha", GachaHandler::gachaRecords);
javalin.get("/gacha/details", GachaHandler::gachaDetails);
javalin.get("/gacha/mappings", ctx -> ctx.result(FileUtils.read(gachaMappingsPath.toString())));
}
}

View File

@@ -4,34 +4,71 @@ import static emu.grasscutter.config.Configuration.ACCOUNT;
import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.objects.WebStaticVersionResponse;
import io.javalin.Javalin;
import io.javalin.http.Context;
/**
* Handles all generic, hard-coded responses.
*/
/** Handles all generic, hard-coded responses. */
public final class GenericHandler implements Router {
@Override public void applyRoutes(Javalin javalin) {
private static void serverStatus(Context ctx) {
int playerCount = Grasscutter.getGameServer().getPlayers().size();
int maxPlayer = ACCOUNT.maxPlayer;
String version = GameConstants.VERSION;
ctx.result(
"{\"retcode\":0,\"status\":{\"playerCount\":"
+ playerCount
+ ",\"maxPlayer\":"
+ maxPlayer
+ ",\"version\":\""
+ version
+ "\"}}");
}
@Override
public void applyRoutes(Javalin javalin) {
// hk4e-sdk-os.hoyoverse.com
javalin.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)
this.allRoutes(javalin, "/hk4e_global/combo/granter/api/compareProtocolVersion", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}"));
javalin.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)
this.allRoutes(
javalin,
"/hk4e_global/combo/granter/api/compareProtocolVersion",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}"));
// api-account-os.hoyoverse.com
javalin.post("/account/risky/api/check", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}"));
javalin.post(
"/account/risky/api/check",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}"));
// sdk-os-static.hoyoverse.com
javalin.get("/combo/box/api/config/sdk/combo", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}"));
javalin.get(
"/combo/box/api/config/sdk/combo",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}"));
// hk4e-sdk-os-static.hoyoverse.com
javalin.get("/hk4e_global/combo/granter/api/getConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}"));
javalin.get(
"/hk4e_global/combo/granter/api/getConfig",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}"));
// hk4e-sdk-os-static.hoyoverse.com
javalin.get("/hk4e_global/mdk/shield/api/loadConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}"));
javalin.get(
"/hk4e_global/mdk/shield/api/loadConfig",
new HttpJsonResponse(
"{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}"));
// Test api?
// abtest-api-data-sg.hoyoverse.com
javalin.post("/data_abtest_api/config/experiment/list", new HttpJsonResponse("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}"));
javalin.post(
"/data_abtest_api/config/experiment/list",
new HttpJsonResponse(
"{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}"));
// log-upload-os.mihoyo.com
this.allRoutes(javalin, "/log/sdk/upload", new HttpJsonResponse("{\"code\":0}"));
@@ -45,12 +82,4 @@ public final class GenericHandler implements Router {
javalin.get("/status/server", GenericHandler::serverStatus);
}
private static void serverStatus(Context ctx) {
int playerCount = Grasscutter.getGameServer().getPlayers().size();
int maxPlayer = ACCOUNT.maxPlayer;
String version = GameConstants.VERSION;
ctx.result("{\"retcode\":0,\"status\":{\"playerCount\":" + playerCount + ",\"maxPlayer\":" + maxPlayer + ",\"version\":\"" + version + "\"}}");
}
}

View File

@@ -4,19 +4,18 @@ import emu.grasscutter.server.http.Router;
import io.javalin.Javalin;
import io.javalin.http.Context;
/**
* Handles logging requests made to the server.
*/
/** Handles logging requests made to the server. */
public final class LogHandler implements Router {
@Override public void applyRoutes(Javalin javalin) {
private static void log(Context ctx) {
// TODO: Figure out how to dump request body and log to file.
ctx.result("{\"code\":0}");
}
@Override
public void applyRoutes(Javalin javalin) {
// overseauspider.yuanshen.com
javalin.post("/log", LogHandler::log);
// log-upload-os.mihoyo.com
javalin.post("/crash/dataUpload", LogHandler::log);
}
private static void log(Context ctx) {
// TODO: Figure out how to dump request body and log to file.
ctx.result("{\"code\":0}");
}
}

View File

@@ -1,15 +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;
}
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;
}
}

View File

@@ -1,17 +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; // ?
}
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; // ?
}
}

View File

@@ -1,31 +1,32 @@
package emu.grasscutter.server.http.objects;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.utils.Utils;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import java.util.Arrays;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.Language.translate;
public final class HttpJsonResponse implements Handler {
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",
"/common/hk4e_global/announcement/api/getAlertAnn",
"/common/hk4e_global/announcement/api/getAnnList",
"/common/hk4e_global/announcement/api/getAnnContent",
"/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier",
"/log/sdk/upload",
"/sdk/upload",
"/perf/config/verify",
"/log",
"/crash/dataUpload"
private final String[]
missingRoutes = { // TODO: When http requests for theses routes are found please remove it
// from this list and update the route request type in the DispatchServer
"/common/hk4e_global/announcement/api/getAlertPic",
"/common/hk4e_global/announcement/api/getAlertAnn",
"/common/hk4e_global/announcement/api/getAnnList",
"/common/hk4e_global/announcement/api/getAnnContent",
"/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier",
"/log/sdk/upload",
"/sdk/upload",
"/perf/config/verify",
"/log",
"/crash/dataUpload"
};
public HttpJsonResponse(String response) {
@@ -35,8 +36,17 @@ public final class HttpJsonResponse implements Handler {
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Checking for ALL here isn't required as when ALL is enabled enableDevLogging() gets enabled
if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING && Arrays.stream(missingRoutes).anyMatch(x -> Objects.equals(x, ctx.endpointHandlerPath()))) {
Grasscutter.getLogger().info(translate("messages.dispatch.request", ctx.ip(), ctx.method(), ctx.endpointHandlerPath()) + (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING ? "(MISSING)" : ""));
if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING
&& Arrays.stream(missingRoutes)
.anyMatch(x -> Objects.equals(x, ctx.endpointHandlerPath()))) {
Grasscutter.getLogger()
.info(
translate(
"messages.dispatch.request",
Utils.address(ctx),
ctx.method(),
ctx.endpointHandlerPath())
+ (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING ? "(MISSING)" : ""));
}
ctx.result(response);
}

View File

@@ -1,7 +1,7 @@
package emu.grasscutter.server.http.objects;
public class LoginAccountRequestJson {
public String account;
public String password;
public boolean is_crypto;
public String account;
public String password;
public boolean is_crypto;
}

View File

@@ -1,38 +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 = "";
}
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 = "";
}
}

View File

@@ -1,6 +1,10 @@
package emu.grasscutter.server.http.objects;
import lombok.Builder;
/** This request object is used in both token-related authenticators. */
@Builder
public class LoginTokenRequestJson {
public String uid;
public String token;
public String uid;
public String token;
}

View File

@@ -1,29 +1,21 @@
package emu.grasscutter.server.http.objects;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.utils.FileUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
import java.io.IOException;
import java.io.InputStream;
public class WebStaticVersionResponse implements Handler {
@Override
public void handle(Context ctx) throws IOException {
String requestFor = ctx.path().substring(ctx.path().lastIndexOf("-") + 1);
getPageResources("/webstatic/" + requestFor, ctx);
return;
}
private static void getPageResources(String path, Context ctx) {
try (InputStream filestream = FileUtils.readResourceAsStream(path)) {
ContentType fromExtension = ContentType.getContentTypeByExtension(path.substring(path.lastIndexOf(".") + 1));
ContentType fromExtension =
ContentType.getContentTypeByExtension(path.substring(path.lastIndexOf(".") + 1));
ctx.contentType(fromExtension != null ? fromExtension : ContentType.APPLICATION_OCTET_STREAM);
ctx.result(filestream.readAllBytes());
} catch (Exception e) {
@@ -33,4 +25,11 @@ public class WebStaticVersionResponse implements Handler {
ctx.status(404);
}
}
@Override
public void handle(Context ctx) throws IOException {
String requestFor = ctx.path().substring(ctx.path().lastIndexOf("-") + 1);
getPageResources("/webstatic/" + requestFor, ctx);
}
}