mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-21 03:15:59 +01:00
Implement proper handbook authentication (pt. 1)
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user