From 008cd06b32719b7bf0500bfc813e7cf590b0d756 Mon Sep 17 00:00:00 2001 From: Furiri Date: Tue, 25 Nov 2025 19:09:32 +0700 Subject: [PATCH] Add Remote Command API (Use KEY) --- src/main/java/emu/nebula/Config.java | 51 ++++---- .../command/commands/RemoteKeyCommand.java | 44 +++++++ .../java/emu/nebula/game/player/Player.java | 3 + src/main/java/emu/nebula/net/GameSession.java | 112 +++++++++--------- .../java/emu/nebula/server/HttpServer.java | 64 ++++++---- .../handlers/HandlerPlayerLoginReq.java | 12 +- .../handlers/HandlerPlayerSignatureEdit.java | 17 +++ .../nebula/server/routes/RemoteHandler.java | 94 +++++++++++++++ 8 files changed, 289 insertions(+), 108 deletions(-) create mode 100644 src/main/java/emu/nebula/command/commands/RemoteKeyCommand.java create mode 100644 src/main/java/emu/nebula/server/routes/RemoteHandler.java diff --git a/src/main/java/emu/nebula/Config.java b/src/main/java/emu/nebula/Config.java index b5021f1..2a95a7c 100644 --- a/src/main/java/emu/nebula/Config.java +++ b/src/main/java/emu/nebula/Config.java @@ -17,10 +17,11 @@ public class Config { public HttpServerConfig httpServer = new HttpServerConfig(80); public GameServerConfig gameServer = new GameServerConfig(80); - + 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"; @@ -54,35 +55,35 @@ public class Config { public int bindPort; public String publicAddress = "127.0.0.1"; // Will return bindAddress if publicAddress is null public Integer publicPort; // Will return bindPort if publicPort is null - + public ServerConfig(int port) { this.bindPort = port; } - + public String getPublicAddress() { if (this.publicAddress != null && !this.publicAddress.isEmpty()) { return this.publicAddress; } - + return this.bindAddress; } - + public int getPublicPort() { if (this.publicPort != null && this.publicPort != 0) { return this.publicPort; } - + return this.bindPort; } - + public String getDisplayAddress() { return (useSSL ? "https" : "http") + "://" + getPublicAddress() + ":" + getPublicPort(); } } - + @Getter public static class HttpServerConfig extends ServerConfig { - + public HttpServerConfig(int port) { super(port); } @@ -90,53 +91,59 @@ public class Config { @Getter public static class GameServerConfig extends ServerConfig { - + public GameServerConfig(int port) { super(port); } } - + @Getter public static class ServerOptions { public Set defaultPermissions = Set.of("*"); 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(); } - + @Getter public static class ServerRates { public double exp = 1.0; } - + @Getter public static class LogOptions { public boolean commands = true; 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; public String sender; public String content; public List attachments; - + public WelcomeMail() { this.title = "Welcome to a Nebula server"; 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)); } } - + } diff --git a/src/main/java/emu/nebula/command/commands/RemoteKeyCommand.java b/src/main/java/emu/nebula/command/commands/RemoteKeyCommand.java new file mode 100644 index 0000000..b1aa60d --- /dev/null +++ b/src/main/java/emu/nebula/command/commands/RemoteKeyCommand.java @@ -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); + + } +} diff --git a/src/main/java/emu/nebula/game/player/Player.java b/src/main/java/emu/nebula/game/player/Player.java index 7929158..6b6def5 100644 --- a/src/main/java/emu/nebula/game/player/Player.java +++ b/src/main/java/emu/nebula/game/player/Player.java @@ -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; diff --git a/src/main/java/emu/nebula/net/GameSession.java b/src/main/java/emu/nebula/net/GameSession.java index 3f750b7..1d4e047 100644 --- a/src/main/java/emu/nebula/net/GameSession.java +++ b/src/main/java/emu/nebula/net/GameSession.java @@ -24,45 +24,49 @@ public class GameSession { private String token; private Account account; private Player player; - + // Crypto private int encryptMethod; // 0 = gcm, 1 = chacha20 private byte[] clientPublicKey; private byte[] serverPublicKey; private byte[] serverPrivateKey; private byte[] key; - + // Session cleanup private boolean remove; private long lastActiveTime; - + public GameSession() { this.updateLastActiveTime(); } - + public synchronized Player getPlayer() { return this.player; } - + public synchronized void setPlayer(Player player) { this.player = player; this.player.setSession(this); this.player.onLogin(); } - + public synchronized void clearPlayer() { // Sanity check if (this.player == null) { return; } - + // Clear player var player = this.player; this.player = null; - + + // Clear remote token + player.setPlayerRemoteToken(null); + player.save(); + // Remove session from player player.removeSession(); - + // Set remove flag this.remove = true; } @@ -70,76 +74,77 @@ public class GameSession { public synchronized boolean hasPlayer() { return this.player != null; } - + // Encryption public void setClientKey(RepeatedByte key) { this.clientPublicKey = key.toArray(); } - + public void generateServerKey() { var pair = AeadHelper.generateECDHKEyPair(); - + this.serverPrivateKey = ((ECPrivateKeyParameters) pair.getPrivate()).getD().toByteArray(); this.serverPublicKey = ((ECPublicKeyParameters) pair.getPublic()).getQ().getEncoded(false); } - + public void calculateKey() { this.key = AeadHelper.generateKey(clientPublicKey, serverPublicKey, serverPrivateKey); this.encryptMethod = Utils.randomRange(0, 1); } - + 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"); byte[] bytes = md.digest(temp.getBytes()); - + this.token = Base64.getEncoder().encodeToString(bytes); } catch (Exception e) { this.token = Base64.getEncoder().encodeToString(temp.getBytes()); } - + return this.token; } - + // Login - + public boolean login(String loginToken) { // Sanity check if (this.account != null) { return false; } - + // Get account this.account = AccountHelper.getAccountByLoginToken(loginToken); - + if (account == null) { 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); - + // Skip intro if (player == null && Nebula.getConfig().getServerOptions().skipIntro) { player = Nebula.getGameContext().getPlayerModule().createPlayer(this, "Player", false); } - + // Set player if (player != null) { this.setPlayer(player); } - + return true; } - + public void updateLastActiveTime() { this.lastActiveTime = System.currentTimeMillis(); } - + // Packet encoding helper functions @SneakyThrows @@ -154,101 +159,98 @@ public class GameSession { this.addNextPackages(proto); } } - + // Encode to message like normal return PacketHelper.encodeMsg(msgId, proto); } - + public byte[] encodeMsg(int msgId) { // Check if we have any packages to send to the client if (this.getPlayer() != null) { // Check if player should add any packages this.checkPlayerStates(); - + // Chain next packages for player if (this.getPlayer().hasNextPackages()) { // Create a proto so we can add next packages var proto = Nil.newInstance(); - + // Encode proto with next packages return this.encodeMsg(msgId, this.addNextPackages(proto)); } } - + // Encode simple message return PacketHelper.encodeMsg(msgId); } - + private void checkPlayerStates() { // Update mail state flag if (this.getPlayer().getMailbox().isNewState()) { // Clear this.getPlayer().getMailbox().clearNewState(); - + // 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()); } } - + private ProtoMessage addNextPackages(ProtoMessage proto) { // Sanity check and make sure proto has a "nextPackage" field if (!PacketHelper.hasNextPackageMethod(proto)) { return proto; } - + // Set next package if (this.getPlayer().getNextPackages().size() > 0) { // Set current package NetMsgPacket curPacket = null; - + // Chain link next packages while (getPlayer().getNextPackages().size() > 0) { // Make sure the current packet has a nextPackage field if (curPacket != null && !PacketHelper.hasNextPackageMethod(curPacket.getProto())) { break; } - + // Get current package var nextPacket = getPlayer().getNextPackages().pop(); - + // Set cur packet if its null if (curPacket == null) { curPacket = nextPacket; continue; } - + // Set next package PacketHelper.setNextPackage(nextPacket.getProto(), curPacket.toByteArray()); - + // Update next packet curPacket = nextPacket; } - + // Set next package of current proto via reflection if (curPacket != null) { PacketHelper.setNextPackage(proto, curPacket.toByteArray()); } } - + return proto; } } diff --git a/src/main/java/emu/nebula/server/HttpServer.java b/src/main/java/emu/nebula/server/HttpServer.java index e4563fa..ad5c183 100644 --- a/src/main/java/emu/nebula/server/HttpServer.java +++ b/src/main/java/emu/nebula/server/HttpServer.java @@ -27,11 +27,11 @@ public class HttpServer { private final Javalin app; private ServerType type; private boolean started; - + // Cached client diff private PatchList patchlist; private byte[] diff; - + public HttpServer(ServerType type) { this.type = type; this.app = Javalin.create(javalinConfig -> { @@ -44,7 +44,7 @@ public class HttpServer { this.loadPatchList(); this.addRoutes(); } - + public HttpServerConfig getServerConfig() { return Nebula.getConfig().getHttpServer(); } @@ -65,26 +65,26 @@ public class HttpServer { sslContextFactory.setRenegotiationAllowed(false); return sslContextFactory; } - + // Patch list - + public long getDataVersion() { return getPatchlist() != null ? getPatchlist().getVersion() : GameConstants.getDataVersion(); } - + public synchronized void loadPatchList() { // Clear this.patchlist = null; this.diff = null; - + // Get file File file = new File(Nebula.getConfig().getPatchListPath()); - + if (!file.exists()) { this.diff = ClientDiff.newInstance().toByteArray(); return; } - + // Load try (FileReader reader = new FileReader(file)) { this.patchlist = JsonUtils.loadToClass(reader, PatchList.class); @@ -93,21 +93,23 @@ public class HttpServer { this.patchlist = null; this.diff = ClientDiff.newInstance().toByteArray(); } - + if (this.patchlist != null) { Nebula.getLogger().info("Loaded patchlist (Data version: " + patchlist.getVersion() + ")"); } } - + // 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); @@ -120,19 +122,20 @@ public class HttpServer { // Done Nebula.getLogger().info("Http Server started on " + getServerConfig().getBindPort()); } - + // Server endpoints private void addRoutes() { + // Add routes if (this.getType().runLogin()) { this.addLoginServerRoutes(); } - + if (this.getType().runGame()) { this.addGameServerRoutes(); } - + // Exception handler getApp().exception(Exception.class, (e, c) -> { e.printStackTrace(); @@ -141,29 +144,40 @@ public class HttpServer { // Fallback handler getApp().error(404, this::notFoundHandler); } - + 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()); getApp().post("/user/login", new UserLoginHandler()); 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() { getApp().post("/agent-zone-1/", new AgentZoneHandler()); } - + private void notFoundHandler(Context ctx) { ctx.status(404); ctx.contentType(ContentType.APPLICATION_JSON); diff --git a/src/main/java/emu/nebula/server/handlers/HandlerPlayerLoginReq.java b/src/main/java/emu/nebula/server/handlers/HandlerPlayerLoginReq.java index 39492a9..7be8ea1 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerPlayerLoginReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerPlayerLoginReq.java @@ -10,7 +10,7 @@ import emu.nebula.net.GameSession; @HandlerId(NetMsgId.player_login_req) public class HandlerPlayerLoginReq extends NetHandler { - + public boolean requirePlayer() { return false; } @@ -20,21 +20,21 @@ public class HandlerPlayerLoginReq extends NetHandler { // Parse request var req = LoginReq.parseFrom(message); var loginToken = req.getOfficialOverseas().getToken(); - + // Login boolean result = session.login(loginToken); - + if (!result) { return session.encodeMsg(NetMsgId.player_login_failed_ack); } - + // Regenerate session token because we are switching encrpytion method Nebula.getGameContext().generateSessionToken(session); - + // Create rsp var rsp = LoginResp.newInstance() .setToken(session.getToken()); - + // Encode and send to client return session.encodeMsg(NetMsgId.player_login_succeed_ack, rsp); } diff --git a/src/main/java/emu/nebula/server/handlers/HandlerPlayerSignatureEdit.java b/src/main/java/emu/nebula/server/handlers/HandlerPlayerSignatureEdit.java index 0b080d0..6825df0 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerPlayerSignatureEdit.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerPlayerSignatureEdit.java @@ -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") diff --git a/src/main/java/emu/nebula/server/routes/RemoteHandler.java b/src/main/java/emu/nebula/server/routes/RemoteHandler.java new file mode 100644 index 0000000..4de42b6 --- /dev/null +++ b/src/main/java/emu/nebula/server/routes/RemoteHandler.java @@ -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 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\"}"); + } +}