Add Remote Command API (Use KEY)

This commit is contained in:
Furiri
2025-11-25 19:09:32 +07:00
committed by Melledy
parent 55ff9b2826
commit 008cd06b32
8 changed files with 289 additions and 108 deletions

View File

@@ -21,6 +21,7 @@ public class Config {
public ServerOptions serverOptions = new ServerOptions();
public ServerRates serverRates = new ServerRates();
public LogOptions logOptions = new LogOptions();
public RemoteCommand remoteCommand = new RemoteCommand();
public int customDataVersion = 0;
public String resourceDir = "./resources";
@@ -102,7 +103,8 @@ public class Config {
public boolean autoCreateAccount = true;
public boolean skipIntro = false;
public boolean unlockInstances = true;
public int sessionTimeout = 600; // How long to wait (in seconds) after the last http request from a session before removing it from the server
public int sessionTimeout = 600; // How long to wait (in seconds) after the last http request from a session
// before removing it from the server
public int dailyResetHour = 0;
public int leaderboardRefreshTime = 60; // Leaderboard refresh time in seconds
public WelcomeMail welcomeMail = new WelcomeMail();
@@ -119,6 +121,12 @@ public class Config {
public boolean packets = false;
}
@Getter
public static class RemoteCommand {
public boolean useRemoteServices = false;
public String serverAdminKey = "HJHASDPIIQWEASDHHAN";
}
@Getter
public static class WelcomeMail {
public String title;
@@ -131,11 +139,10 @@ public class Config {
this.sender = "Server";
this.content = "Welcome to Nebula! Please take these items as a starter gift.";
this.attachments = List.of(
new ItemParam(86009, 1),
new ItemParam(86002, 1),
new ItemParam(1, 1_000_000),
new ItemParam(2, 30_000)
);
new ItemParam(86009, 1),
new ItemParam(86002, 1),
new ItemParam(1, 1_000_000),
new ItemParam(2, 30_000));
}
}

View File

@@ -0,0 +1,44 @@
package emu.nebula.command.commands;
import emu.nebula.Nebula;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import java.util.Random;
@Command(label = "remote", permission = "player.remote", requireTarget = true, desc = "/remote. Send remote to web remote")
public class RemoteKeyCommand implements CommandHandler {
private static String lastMessage;
public static String getLastMessage() {
return lastMessage;
}
@Override
public void execute(CommandArgs args) {
if (Nebula.getConfig().getRemoteCommand().useRemoteServices) {
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 8; i++) {
int index = random.nextInt(characters.length());
sb.append(characters.charAt(index));
}
args.getTarget().setPlayerRemoteToken(sb.toString());
args.getTarget().save();
String textsend = "Key Generated: " + sb.toString();
lastMessage = textsend;
args.sendMessage(textsend);
return;
}
String textsend = "RemoteCommand Disabled on Server";
args.getTarget().setPlayerRemoteToken(null);
args.getTarget().save();
lastMessage = textsend;
args.sendMessage(textsend);
}
}

View File

@@ -45,6 +45,7 @@ import emu.nebula.proto.Public.WorldClassRewardState;
import emu.nebula.proto.Public.Title;
import lombok.Getter;
import lombok.Setter;
import us.hebi.quickbuf.ProtoMessage;
import us.hebi.quickbuf.RepeatedInt;
@@ -87,6 +88,7 @@ public class Player implements GameDatabaseObject {
private final transient InfinityTowerManager infinityTowerManager;
private final transient VampireSurvivorManager vampireSurvivorManager;
private final transient ScoreBossManager scoreBossManager;
@Indexed @Setter @Getter private String playerRemoteToken;
// Referenced data
private transient Inventory inventory;
@@ -142,6 +144,7 @@ public class Player implements GameDatabaseObject {
this.honor = new int[3];
this.showChars = new int[3];
this.boards = new int[] {410301};
this.playerRemoteToken = null;
this.level = 1;
this.energy = 240;

View File

@@ -60,6 +60,10 @@ public class GameSession {
var player = this.player;
this.player = null;
// Clear remote token
player.setPlayerRemoteToken(null);
player.save();
// Remove session from player
player.removeSession();
@@ -90,7 +94,7 @@ public class GameSession {
}
public String generateToken() {
String temp = System.currentTimeMillis() + ":" + AeadHelper.generateBytes(64).toString();
String temp = System.currentTimeMillis() + ":" + AeadHelper.generateBytes(64).toString();
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
@@ -119,7 +123,8 @@ public class GameSession {
return false;
}
// Note: We should cache players in case multiple sessions try to login to the same player at the time
// Note: We should cache players in case multiple sessions try to login to the
// same player at the time
// Get player by account
var player = Nebula.getGameContext().getPlayerModule().loadPlayer(account);
@@ -187,25 +192,22 @@ public class GameSession {
// Send mail state notify
this.getPlayer().addNextPackage(
NetMsgId.mail_state_notify,
MailState.newInstance().setNew(true)
);
NetMsgId.mail_state_notify,
MailState.newInstance().setNew(true));
}
// Check handbook states
if (this.getPlayer().getCharacters().isUpdateCharHandbook()) {
getPlayer().getCharacters().setUpdateCharHandbook(false);
getPlayer().addNextPackage(
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getCharacterHandbook()
);
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getCharacterHandbook());
}
if (this.getPlayer().getCharacters().isUpdateDiscHandbook()) {
getPlayer().getCharacters().setUpdateDiscHandbook(false);
getPlayer().addNextPackage(
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getDiscHandbook()
);
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getDiscHandbook());
}
}

View File

@@ -102,12 +102,14 @@ public class HttpServer {
// Start server
public void start() {
if (this.started) return;
if (this.started)
return;
this.started = true;
// Http server
if (getServerConfig().isUseSSL()) {
ServerConnector sslConnector = new ServerConnector(getApp().jettyServer().server(), getSSLContextFactory(), getHttpFactory());
ServerConnector sslConnector = new ServerConnector(getApp().jettyServer().server(), getSSLContextFactory(),
getHttpFactory());
sslConnector.setHost(getServerConfig().getBindAddress());
sslConnector.setPort(getServerConfig().getBindPort());
getApp().jettyServer().server().addConnector(sslConnector);
@@ -124,6 +126,7 @@ public class HttpServer {
// Server endpoints
private void addRoutes() {
// Add routes
if (this.getType().runLogin()) {
this.addLoginServerRoutes();
@@ -145,7 +148,8 @@ public class HttpServer {
private void addLoginServerRoutes() {
// https://en-sdk-api.yostarplat.com/
getApp().post("/common/config", new CommonConfigHandler(this));
getApp().post("/common/version", new HttpJsonResponse("{\"Code\":200,\"Data\":{\"Agreement\":[{\"Version\":\"0.1\",\"Type\":\"user_agreement\",\"Title\":\"用户协议\",\"Content\":\"\",\"Lang\":\"en\"},{\"Version\":\"0.1\",\"Type\":\"privacy_agreement\",\"Title\":\"隐私政策\",\"Content\":\"\",\"Lang\":\"en\"}],\"ErrorCode\":\"4.4\"},\"Msg\":\"OK\"}"));
getApp().post("/common/version", new HttpJsonResponse(
"{\"Code\":200,\"Data\":{\"Agreement\":[{\"Version\":\"0.1\",\"Type\":\"user_agreement\",\"Title\":\"用户协议\",\"Content\":\"\",\"Lang\":\"en\"},{\"Version\":\"0.1\",\"Type\":\"privacy_agreement\",\"Title\":\"隐私政策\",\"Content\":\"\",\"Lang\":\"en\"}],\"ErrorCode\":\"4.4\"},\"Msg\":\"OK\"}"));
getApp().post("/user/detail", new UserLoginHandler());
getApp().post("/user/set", new UserSetDataHandler());
@@ -153,11 +157,21 @@ public class HttpServer {
getApp().post("/user/quick-login", new UserLoginHandler());
getApp().post("/yostar/get-auth", new GetAuthHandler());
getApp().post("/yostar/send-code", new HttpJsonResponse("{\"Code\":200,\"Data\":{},\"Msg\":\"OK\"}")); // Dummy handler
getApp().post("/yostar/send-code", new HttpJsonResponse("{\"Code\":200,\"Data\":{},\"Msg\":\"OK\"}")); // Dummy
// handler
// https://nova-static.stellasora.global/
getApp().get("/meta/serverlist.html", new MetaServerlistHandler(this));
getApp().get("/meta/win.html", new MetaWinHandler(this));
// if (!Nebula.getConfig().getRemoteCommand().useRemoteServices) {
// getApp().post("/api/command", new RemoteHandler());
// }
getApp().post("/api/command", new RemoteHandler());
// getApp.get("/notice/noticelist.html");
getApp().get("/webchatv3/*", ctx -> {
ctx.redirect("https://google.com");
});
}
private void addGameServerRoutes() {

View File

@@ -23,7 +23,24 @@ public class HandlerPlayerSignatureEdit extends NetHandler {
// Check if we need to handle a command
if (signature.charAt(0) == '!' || signature.charAt(0) == '/') {
String commandLabel = signature.toLowerCase().trim();
if (commandLabel.startsWith("!") || commandLabel.startsWith("/")) {
commandLabel = commandLabel.substring(1).split(" ")[0];
}
Nebula.getCommandManager().invoke(session.getPlayer(), signature);
// If this is the remote command, return the message
if ("remote".equals(commandLabel)) {
String remoteMessage = emu.nebula.command.commands.RemoteKeyCommand.getLastMessage();
if (remoteMessage != null) {
return session.encodeMsg(
NetMsgId.player_signature_edit_failed_ack,
Error.newInstance().setCode(119902).addArguments("\n" + remoteMessage)
);
}
}
return session.encodeMsg(
NetMsgId.player_signature_edit_failed_ack,
Error.newInstance().setCode(119902).addArguments("\nCommand Success")

View File

@@ -0,0 +1,94 @@
package emu.nebula.server.routes;
import emu.nebula.Nebula;
import emu.nebula.game.player.Player;
import emu.nebula.util.JsonUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import org.jetbrains.annotations.NotNull;
public class RemoteHandler implements Handler {
static class RemoteCommandRequest {
public String token;
public String command;
}
// Cache: Token -> UID
private static final java.util.Map<String, Integer> tokenCache = new java.util.concurrent.ConcurrentHashMap<>();
@Override
public void handle(@NotNull Context ctx) throws Exception {
if (!Nebula.getConfig().getRemoteCommand().useRemoteServices) {
ctx.status(403);
ctx.result("{\"Code\":403,\"Msg\":\"RemoteServer not enable\"}");
return;
}
// Parse body
RemoteCommandRequest req = JsonUtils.decode(ctx.body(), RemoteCommandRequest.class);
if (req == null || req.token == null || req.command == null) {
ctx.status(400);
ctx.result("{\"Code\":400,\"Msg\":\"Invalid request\"}");
return;
}
String token = req.token;
String command = req.command;
String adminKey = Nebula.getConfig().getRemoteCommand().getServerAdminKey();
// Check admin key
if (token.equals(adminKey)) {
Nebula.getCommandManager().invoke(null, command);
Nebula.getLogger().warn(
"\u001B[38;2;252;186;3mRemote Server (Using Admin Key) sent command: /" + command + "\u001B[0m");
ctx.status(200);
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":200,\"Data\":{},\"Msg\":\"Command executed\"}");
return;
}
// Check player
Player player = null;
// 1. Try cache
Integer cachedUid = tokenCache.get(token);
if (cachedUid != null) {
player = Nebula.getGameContext().getPlayerModule().getPlayer(cachedUid);
// Verify token matches (in case player changed token or cache is stale)
if (player != null && !token.equals(player.getPlayerRemoteToken())) {
player = null;
tokenCache.remove(token);
}
}
// 2. Fallback to DB if not in cache or cache invalid
if (player == null) {
player = Nebula.getGameDatabase().getObjectByField(Player.class, "playerRemoteToken", token);
if (player != null) {
tokenCache.put(token, player.getUid());
}
}
if (player != null) {
// Append target UID to command to ensure it targets the player
// CommandArgs parses @UID to set the target
String finalCommand = command + " @" + player.getUid();
Nebula.getLogger().info("Remote Player Request [" + player.getUid() + "]: " + finalCommand);
// Execute as console (null sender) but targeting the player
Nebula.getCommandManager().invoke(null, finalCommand);
ctx.status(200);
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":200,\"Data\":{},\"Msg\":\"Command executed\"}");
return;
}
// Invalid token
ctx.status(403);
ctx.result("{\"Code\":403,\"Msg\":\"Invalid token\"}");
}
}