mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-19 10:24:47 +01:00
Implement proper handbook authentication (pt. 1)
This commit is contained in:
@@ -4,11 +4,12 @@ import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.server.http.objects.*;
|
||||
import emu.grasscutter.utils.DispatchUtils;
|
||||
import io.javalin.http.Context;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Defines an authenticator for the server. Can be changed by plugins. */
|
||||
public interface AuthenticationSystem {
|
||||
|
||||
@@ -130,6 +131,13 @@ public interface AuthenticationSystem {
|
||||
*/
|
||||
OAuthAuthenticator getOAuthAuthenticator();
|
||||
|
||||
/**
|
||||
* This is the authenticator used for handling handbook authentication requests.
|
||||
*
|
||||
* @return An authenticator.
|
||||
*/
|
||||
HandbookAuthenticator getHandbookAuthenticator();
|
||||
|
||||
/** A data container that holds relevant data for authenticating a client. */
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package emu.grasscutter.auth;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.auth.DefaultAuthenticators.*;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
||||
import emu.grasscutter.server.http.objects.LoginResultJson;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
/**
|
||||
* The default Grasscutter authentication implementation. Allows all users to access any account.
|
||||
*/
|
||||
@@ -20,6 +20,7 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
||||
private final Authenticator<Account> sessionTokenValidator = new SessionTokenValidator();
|
||||
private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication();
|
||||
private final OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication();
|
||||
private final HandbookAuthenticator handbookAuthenticator = new HandbookAuthentication();
|
||||
|
||||
public DefaultAuthentication() {
|
||||
if (ACCOUNT.EXPERIMENTAL_RealPassword) {
|
||||
@@ -75,4 +76,9 @@ public final class DefaultAuthentication implements AuthenticationSystem {
|
||||
public OAuthAuthenticator getOAuthAuthenticator() {
|
||||
return this.oAuthAuthenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandbookAuthenticator getHandbookAuthenticator() {
|
||||
return this.handbookAuthenticator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package emu.grasscutter.auth;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
import at.favre.lib.crypto.bcrypt.BCrypt;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||
@@ -12,15 +9,21 @@ import emu.grasscutter.server.dispatch.IDispatcher;
|
||||
import emu.grasscutter.server.dispatch.PacketIds;
|
||||
import emu.grasscutter.server.http.objects.ComboTokenResJson;
|
||||
import emu.grasscutter.server.http.objects.LoginResultJson;
|
||||
import emu.grasscutter.utils.DispatchUtils;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import io.javalin.http.ContentType;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.ACCOUNT;
|
||||
import static emu.grasscutter.utils.Language.translate;
|
||||
|
||||
/** A class containing default authenticators. */
|
||||
public final class DefaultAuthenticators {
|
||||
@@ -372,4 +375,61 @@ public final class DefaultAuthenticators {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles authentication for the web GM Handbook. */
|
||||
public static class HandbookAuthentication implements HandbookAuthenticator {
|
||||
private final String authPage;
|
||||
|
||||
public HandbookAuthentication() {
|
||||
try {
|
||||
this.authPage = new String(
|
||||
FileUtils.readResource("/html/handbook_auth.html"));
|
||||
} catch (Exception ignored) {
|
||||
throw new RuntimeException("Failed to load handbook auth page.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void presentPage(AuthenticationRequest request) {
|
||||
var ctx = request.getContext();
|
||||
if (ctx == null) return;
|
||||
|
||||
// Respond with the handbook auth page.
|
||||
ctx.contentType(ContentType.TEXT_HTML)
|
||||
.result(this.authPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response authenticate(AuthenticationRequest request) {
|
||||
var ctx = request.getContext();
|
||||
if (ctx == null) return null;
|
||||
|
||||
// Get the body data.
|
||||
var playerId = ctx.formParam("playerid");
|
||||
if (playerId == null) {
|
||||
return Response.builder().status(400)
|
||||
.body("Invalid player ID.").build();
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the player's session token.
|
||||
var sessionKey = DispatchUtils.fetchSessionKey(
|
||||
Integer.parseInt(playerId));
|
||||
if (sessionKey == null) {
|
||||
return Response.builder().status(400)
|
||||
.body("Invalid player ID.").build();
|
||||
}
|
||||
|
||||
// Check if the account is banned.
|
||||
return Response.builder().status(200)
|
||||
.body(this.authPage.replace("{{VALUE}}", "true")
|
||||
.replace("{{SESSION_TOKEN}}", sessionKey)
|
||||
.replace("{{PLAYER_ID}}", playerId))
|
||||
.build();
|
||||
} catch (NumberFormatException ignored) {
|
||||
return Response.builder().status(500)
|
||||
.body("Invalid player ID.").build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package emu.grasscutter.auth;
|
||||
|
||||
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/** Handles player authentication for the web GM handbook. */
|
||||
public interface HandbookAuthenticator {
|
||||
@Getter @Builder
|
||||
class Response {
|
||||
private final int status;
|
||||
private final String body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the user requests to authenticate.
|
||||
* This should respond with a page that allows the user to authenticate.
|
||||
*
|
||||
* @route GET /handbook/authenticate
|
||||
* @param request The authentication request.
|
||||
*/
|
||||
void presentPage(AuthenticationRequest request);
|
||||
|
||||
/**
|
||||
* Invoked when the user requests to authenticate.
|
||||
* This is called when the user submits the authentication form.
|
||||
* This should respond with HTML that sends a message to the GM Handbook.
|
||||
* See the default handbook authentication page for an example.
|
||||
*
|
||||
* @param request The authentication request.
|
||||
* @return The response to send to the client.
|
||||
*/
|
||||
Response authenticate(AuthenticationRequest request);
|
||||
}
|
||||
@@ -94,7 +94,8 @@ import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
@Entity(value = "players", useDiscriminator = false)
|
||||
public class Player implements PlayerHook {
|
||||
@Id private int id;
|
||||
@Indexed(options = @IndexOptions(unique = true)) private String accountId;
|
||||
@Indexed(options = @IndexOptions(unique = true))
|
||||
@Getter private String accountId;
|
||||
@Setter private transient Account account;
|
||||
@Getter @Setter private transient GameSession session;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package emu.grasscutter.server.dispatch;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
@@ -12,21 +10,25 @@ import emu.grasscutter.utils.Crypto;
|
||||
import emu.grasscutter.utils.DispatchUtils;
|
||||
import emu.grasscutter.utils.JsonUtils;
|
||||
import emu.grasscutter.utils.objects.HandbookBody;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.Getter;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.java_websocket.client.WebSocketClient;
|
||||
import org.java_websocket.handshake.ServerHandshake;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
public final class DispatchClient extends WebSocketClient implements IDispatcher {
|
||||
@Getter private final Logger logger = Grasscutter.getLogger();
|
||||
@Getter private final Map<Integer, BiConsumer<WebSocket, JsonElement>> handlers = new HashMap<>();
|
||||
@@ -41,6 +43,7 @@ public final class DispatchClient extends WebSocketClient implements IDispatcher
|
||||
|
||||
this.registerHandler(PacketIds.GachaHistoryReq, this::fetchGachaHistory);
|
||||
this.registerHandler(PacketIds.GmTalkReq, this::handleHandbookAction);
|
||||
this.registerHandler(PacketIds.GetPlayerFieldsReq, this::fetchPlayerFields);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,6 +108,32 @@ public final class DispatchClient extends WebSocketClient implements IDispatcher
|
||||
this.sendMessage(PacketIds.GmTalkRsp, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the fields of an online player.
|
||||
*
|
||||
* @param socket The socket the packet was received from.
|
||||
* @param object The packet data.
|
||||
*/
|
||||
private void fetchPlayerFields(WebSocket socket, JsonElement object) {
|
||||
var message = IDispatcher.decode(object);
|
||||
var playerId = message.get("playerId").getAsInt();
|
||||
var fieldsRaw = message.get("fields").getAsJsonArray();
|
||||
|
||||
// Get the player with the specified ID.
|
||||
var player = Grasscutter.getGameServer().getPlayerByUid(playerId, true);
|
||||
if (player == null) return;
|
||||
|
||||
// Convert the fields array.
|
||||
var fieldsList = new ArrayList<String>();
|
||||
for (var field : fieldsRaw)
|
||||
fieldsList.add(field.getAsString());
|
||||
var fields = fieldsList.toArray(new String[0]);
|
||||
|
||||
// Return the response object.
|
||||
this.sendMessage(PacketIds.GetPlayerFieldsRsp,
|
||||
DispatchUtils.getPlayerFields(playerId, fields));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a serialized encrypted message to the server.
|
||||
*
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package emu.grasscutter.server.dispatch;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.utils.Crypto;
|
||||
import lombok.Getter;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.java_websocket.handshake.ClientHandshake;
|
||||
import org.java_websocket.server.WebSocketServer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -15,11 +19,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.Getter;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.java_websocket.handshake.ClientHandshake;
|
||||
import org.java_websocket.server.WebSocketServer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
/* Internal communications server. */
|
||||
public final class DispatchServer extends WebSocketServer implements IDispatcher {
|
||||
@@ -39,6 +40,7 @@ public final class DispatchServer extends WebSocketServer implements IDispatcher
|
||||
|
||||
this.registerHandler(PacketIds.LoginNotify, this::handleLogin);
|
||||
this.registerHandler(PacketIds.TokenValidateReq, this::validateToken);
|
||||
this.registerHandler(PacketIds.GetAccountReq, this::fetchAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,6 +86,23 @@ public final class DispatchServer extends WebSocketServer implements IDispatcher
|
||||
this.sendMessage(socket, PacketIds.TokenValidateRsp, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an account by its ID.
|
||||
*
|
||||
* @param socket The socket the packet was received from.
|
||||
* @param object The packet data.
|
||||
*/
|
||||
private void fetchAccount(WebSocket socket, JsonElement object) {
|
||||
var message = IDispatcher.decode(object);
|
||||
var accountId = message.get("accountId").getAsString();
|
||||
|
||||
// Get the account from the database.
|
||||
var account = DatabaseHelper.getAccountById(accountId);
|
||||
// Send the account.
|
||||
this.sendMessage(socket, PacketIds.GetAccountRsp,
|
||||
JSON.toJsonTree(account));
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an encrypted message to all connected clients.
|
||||
*
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
package emu.grasscutter.server.dispatch;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import emu.grasscutter.utils.Crypto;
|
||||
import emu.grasscutter.utils.JsonAdapters.ByteArrayAdapter;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.slf4j.Logger;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
public interface IDispatcher {
|
||||
Gson JSON =
|
||||
@@ -24,6 +28,9 @@ public interface IDispatcher {
|
||||
.registerTypeAdapter(byte[].class, new ByteArrayAdapter())
|
||||
.create();
|
||||
|
||||
Function<JsonElement, JsonObject> DEFAULT_PARSER = (packet) ->
|
||||
IDispatcher.decode(packet, JsonObject.class);
|
||||
|
||||
/**
|
||||
* Decodes an escaped JSON message.
|
||||
*
|
||||
@@ -61,6 +68,75 @@ public interface IDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a request from the other server to be fulfilled.
|
||||
*
|
||||
* @param request The request data.
|
||||
* @param requestId The request packet ID.
|
||||
* @param responseId the response packet ID.
|
||||
* @param parser The parser for the response data.
|
||||
* @return The fulfilled data, or null.
|
||||
* @param <T> The type of data to be returned.
|
||||
*/
|
||||
default <T> T await(JsonObject request, int requestId, int responseId,
|
||||
Function<JsonElement, T> parser) {
|
||||
// Perform the setup for the request.
|
||||
var future = this.async(request, requestId, responseId, parser);
|
||||
|
||||
try {
|
||||
// Try to return the value.
|
||||
return future.get(5L, TimeUnit.SECONDS);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback for a packet to be received.
|
||||
* Sends a packet with the provided request.
|
||||
*
|
||||
* @param request The request object.
|
||||
* @param requestId The packet ID of the request packet.
|
||||
* @param responseId The packet ID of the response packet.
|
||||
* @return A promise containing the parsed JSON data.
|
||||
*/
|
||||
default CompletableFuture<JsonObject> async(JsonObject request, int requestId, int responseId) {
|
||||
return this.async(request, requestId, responseId, DEFAULT_PARSER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback for a packet to be received.
|
||||
* Sends a packet with the provided request.
|
||||
*
|
||||
* @param request The request object.
|
||||
* @param requestId The packet ID of the request packet.
|
||||
* @param responseId The packet ID of the response packet.
|
||||
* @param parser The parser for the received data.
|
||||
* @return A promise containing the parsed JSON data.
|
||||
*/
|
||||
default <T> CompletableFuture<T> async(
|
||||
JsonObject request, int requestId, int responseId,
|
||||
Function<JsonElement, T> parser
|
||||
) {
|
||||
// Create the future.
|
||||
var future = new CompletableFuture<T>();
|
||||
// Listen for the response.
|
||||
this.registerCallback(responseId, packet ->
|
||||
future.complete(parser.apply(packet)));
|
||||
// Broadcast the packet.
|
||||
this.sendMessage(requestId, request);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally used method to broadcast a packet.
|
||||
*
|
||||
* @param packetId The packet ID.
|
||||
* @param message The packet data.
|
||||
*/
|
||||
void sendMessage(int packetId, Object message);
|
||||
|
||||
/**
|
||||
* Decodes a message from the client.
|
||||
*
|
||||
|
||||
@@ -11,4 +11,8 @@ public interface PacketIds {
|
||||
int GachaHistoryRsp = 5;
|
||||
int GmTalkReq = PacketOpcodes.GmTalkReq;
|
||||
int GmTalkRsp = PacketOpcodes.GmTalkRsp;
|
||||
int GetAccountReq = 6;
|
||||
int GetAccountRsp = 7;
|
||||
int GetPlayerFieldsReq = 8;
|
||||
int GetPlayerFieldsRsp = 9;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import emu.grasscutter.Grasscutter.ServerDebugMode;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import io.javalin.Javalin;
|
||||
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;
|
||||
@@ -57,6 +58,9 @@ public final class HttpServer {
|
||||
if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL)
|
||||
config.plugins.enableDevLogging();
|
||||
|
||||
// Set the JSON mapper.
|
||||
config.jsonMapper(new JavalinGson());
|
||||
|
||||
// Static files should be added like this https://javalin.io/documentation#static-files
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
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.DispatchUtils;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import emu.grasscutter.utils.objects.HandbookBody;
|
||||
import emu.grasscutter.utils.objects.HandbookBody.Action;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.ContentType;
|
||||
import io.javalin.http.Context;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.HANDBOOK;
|
||||
|
||||
/** Handles requests for the new GM Handbook. */
|
||||
public final class HandbookHandler implements Router {
|
||||
private final byte[] handbook;
|
||||
@@ -20,7 +23,7 @@ public final class HandbookHandler implements Router {
|
||||
* found.
|
||||
*/
|
||||
public HandbookHandler() {
|
||||
this.handbook = FileUtils.readResource("/handbook.html");
|
||||
this.handbook = FileUtils.readResource("/html/handbook.html");
|
||||
this.serve = HANDBOOK.enable && this.handbook.length > 0;
|
||||
}
|
||||
|
||||
@@ -30,6 +33,9 @@ public final class HandbookHandler implements Router {
|
||||
|
||||
// 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);
|
||||
@@ -59,6 +65,49 @@ public final class HandbookHandler implements Router {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.getBody().contains("html") ?
|
||||
ContentType.TEXT_HTML : ContentType.TEXT_PLAIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants the avatar to the user.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package emu.grasscutter.utils;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
|
||||
@@ -14,10 +13,16 @@ import emu.grasscutter.server.http.handlers.GachaHandler;
|
||||
import emu.grasscutter.server.http.objects.LoginTokenRequestJson;
|
||||
import emu.grasscutter.utils.objects.HandbookBody;
|
||||
import emu.grasscutter.utils.objects.HandbookBody.*;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.DISPATCH_INFO;
|
||||
|
||||
public interface DispatchUtils {
|
||||
/** HTTP client used for dispatch queries. */
|
||||
@@ -62,6 +67,132 @@ public interface DispatchUtils {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the session key for the specified player ID.
|
||||
*
|
||||
* @param playerId The player ID.
|
||||
* @return The session key.
|
||||
*/
|
||||
@Nullable static String fetchSessionKey(int playerId) {
|
||||
return switch (Grasscutter.getRunMode()) {
|
||||
case GAME_ONLY -> {
|
||||
// Fetch the player from the game server.
|
||||
var player = DatabaseHelper.getPlayerByUid(playerId);
|
||||
if (player == null) yield null;
|
||||
|
||||
// Fetch the account from the dispatch server.
|
||||
var accountId = player.getAccountId();
|
||||
var account = DispatchUtils.getAccountById(accountId);
|
||||
|
||||
// Return the session key.
|
||||
yield account == null ? null : account.getSessionKey();
|
||||
}
|
||||
case DISPATCH_ONLY -> {
|
||||
// Fetch the player's account ID from the game server.
|
||||
var playerFields = DispatchUtils.getPlayerFields(playerId, "accountId");
|
||||
if (playerFields == null) yield null;
|
||||
|
||||
// Get the account ID.
|
||||
var accountId = playerFields.get("accountId").getAsString();
|
||||
if (accountId == null) yield null;
|
||||
|
||||
// Fetch the account from the dispatch server.
|
||||
var account = DatabaseHelper.getAccountById(accountId);
|
||||
// Return the session key.
|
||||
yield account == null ? null : account.getSessionKey();
|
||||
}
|
||||
case HYBRID -> {
|
||||
// Fetch the player from the game server.
|
||||
var player = DatabaseHelper.getPlayerByUid(playerId);
|
||||
if (player == null) yield null;
|
||||
|
||||
// Fetch the account from the database.
|
||||
var account = player.getAccount();
|
||||
// Return the session key.
|
||||
yield account == null ? null : account.getSessionKey();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an account by its ID.
|
||||
*
|
||||
* @param accountId The account ID.
|
||||
* @return The account.
|
||||
*/
|
||||
@Nullable static Account getAccountById(String accountId) {
|
||||
return switch (Grasscutter.getRunMode()) {
|
||||
case GAME_ONLY -> {
|
||||
// Create a request for account information.
|
||||
var request = new JsonObject();
|
||||
request.addProperty("accountId", accountId);
|
||||
|
||||
// Wait for the request to complete.
|
||||
yield Grasscutter.getGameServer().getDispatchClient()
|
||||
.await(request, PacketIds.GetAccountReq, PacketIds.GetAccountRsp,
|
||||
packet -> IDispatcher.decode(packet, Account.class));
|
||||
}
|
||||
case HYBRID, DISPATCH_ONLY ->
|
||||
DatabaseHelper.getAccountById(accountId);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the values of fields for a player.
|
||||
*
|
||||
* @param playerId The player's ID.
|
||||
* @param fields The fields to fetch.
|
||||
* @return An object holding the field values.
|
||||
*/
|
||||
@Nullable static JsonObject getPlayerFields(int playerId, String... fields) {
|
||||
return switch (Grasscutter.getRunMode()) {
|
||||
case DISPATCH_ONLY -> {
|
||||
// Create a request for player fields.
|
||||
var request = new JsonObject();
|
||||
request.addProperty("playerId", playerId);
|
||||
request.add("fields", IDispatcher.JSON.toJsonTree(fields));
|
||||
|
||||
// Wait for the request to complete.
|
||||
yield Grasscutter.getDispatchServer()
|
||||
.await(request, PacketIds.GetPlayerFieldsReq, PacketIds.GetPlayerFieldsRsp,
|
||||
IDispatcher.DEFAULT_PARSER);
|
||||
}
|
||||
case HYBRID, GAME_ONLY -> {
|
||||
// Get the player by ID.
|
||||
var player = Grasscutter.getGameServer()
|
||||
.getPlayerByUid(playerId, true);
|
||||
if (player == null) yield null;
|
||||
|
||||
// Prepare field properties.
|
||||
var fieldValues = new JsonObject();
|
||||
var fieldMap = new HashMap<String, Field>();
|
||||
Arrays.stream(player.getClass().getDeclaredFields())
|
||||
.forEach(field -> fieldMap.put(field.getName(), field));
|
||||
|
||||
// Find the values of all requested fields.
|
||||
for (var fieldName : fields) {
|
||||
try {
|
||||
var field = fieldMap.get(fieldName);
|
||||
if (field == null)
|
||||
fieldValues.add(fieldName, JsonNull.INSTANCE);
|
||||
else {
|
||||
var wasAccessible = field.canAccess(player);
|
||||
field.setAccessible(true);
|
||||
fieldValues.add(fieldName,
|
||||
IDispatcher.JSON.toJsonTree(field.get(player)));
|
||||
field.setAccessible(wasAccessible);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Return the values.
|
||||
yield fieldValues;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the gacha history for the specified account.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ public interface HandbookBody {
|
||||
@Getter
|
||||
class GrantAvatar {
|
||||
private String player; // Parse into online player ID.
|
||||
private String playerToken; // Parse into session token.
|
||||
private String avatar; // Parse into avatar ID.
|
||||
|
||||
private int level = 90; // Range between 1 - 90.
|
||||
@@ -33,6 +34,7 @@ public interface HandbookBody {
|
||||
@Getter
|
||||
class GiveItem {
|
||||
private String player; // Parse into online player ID.
|
||||
private String playerToken; // Parse into session token.
|
||||
private String item; // Parse into item ID.
|
||||
|
||||
private long amount = 1; // Range between 1 - Long.MAX_VALUE.
|
||||
@@ -41,12 +43,14 @@ public interface HandbookBody {
|
||||
@Getter
|
||||
class TeleportTo {
|
||||
private String player; // Parse into online player ID.
|
||||
private String playerToken; // Parse into session token.
|
||||
private String scene; // Parse into a scene ID.
|
||||
}
|
||||
|
||||
@Getter
|
||||
class SpawnEntity {
|
||||
private String player; // Parse into online player ID.
|
||||
private String playerToken; // Parse into session token.
|
||||
private String entity; // Parse into entity ID.
|
||||
|
||||
private long amount = 1; // Range between 1 - Long.MAX_VALUE.
|
||||
|
||||
Reference in New Issue
Block a user