From 5fadc725d0dbbeed78dbcb78b9484306d1001a2b Mon Sep 17 00:00:00 2001 From: Sen Date: Fri, 6 Jun 2025 02:51:34 +0200 Subject: [PATCH] add basic user saving --- proxy/src/main/java/proxy/Proxy.java | 85 ++++++++++++++++++- .../src/main/java/proxy/command/Command.java | 3 + .../main/java/proxy/command/CommandAdmin.java | 4 + .../java/proxy/command/CommandDelete.java | 2 +- .../main/java/proxy/command/CommandPing.java | 5 +- .../java/proxy/command/CommandRevoke.java | 12 ++- .../main/java/proxy/handler/ProxyHandler.java | 15 ++-- proxy/src/main/java/proxy/util/Formatter.java | 23 +++++ .../src/main/java/proxy/util/Permissions.java | 7 ++ proxy/src/main/java/proxy/util/User.java | 64 ++++++++++++++ 10 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 proxy/src/main/java/proxy/util/Permissions.java diff --git a/proxy/src/main/java/proxy/Proxy.java b/proxy/src/main/java/proxy/Proxy.java index 17d445b..457928c 100755 --- a/proxy/src/main/java/proxy/Proxy.java +++ b/proxy/src/main/java/proxy/Proxy.java @@ -49,11 +49,17 @@ import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; +import com.google.common.io.Files; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFutureTask; import com.google.common.util.concurrent.ThreadFactoryBuilder; - +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; import proxy.network.Direction; import proxy.network.Decoder; import proxy.network.Splitter; @@ -85,6 +91,8 @@ public class Proxy { return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build()); } }; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final JsonParser PARSER = new JsonParser(); private volatile boolean isAlive; private volatile boolean running = true; @@ -153,7 +161,44 @@ public class Proxy { this.registerCommands(); } + private static JsonObject loadFile(File file) { + if(!file.exists()) + return new JsonObject(); + String data; + try { + data = Files.toString(file, Charsets.UTF_8); + } + catch(IOException e) { + Log.error(e, "Could not load %s", file); + return new JsonObject(); + } + JsonElement elem; + try { + elem = PARSER.parse(data); + } + catch(JsonParseException e) { + Log.error(e, "Could not parse %s", file); + return new JsonObject(); + } + if(!elem.isJsonObject()) { + Log.error("Could not parse %s: root is not a JSON object", file); + return new JsonObject(); + } + return elem.getAsJsonObject(); + } + private void loadConfig() { + JsonObject obj = loadFile(new File("vproxy.json")); + // ... + + obj = loadFile(new File("users.json")); + if(obj.has("users") && obj.get("users").isJsonArray()) { + for(User user : User.fromJson(obj.get("users").getAsJsonArray())) { + this.users.put(user.getUsername().toLowerCase(Locale.US), user); + } + } + // ... + this.status = new ServerInfo("VLoginProxy 1.8.9", 47); this.status.setCapacity(this.maxPlayers); Collections.addAll(this.status.getMotds(), Formatter.DARK_RED + "Miau\n" + Formatter.YELLOW + "Test??", Formatter.AQUA + "Server\n" + Formatter.GREEN + "Test!!"); @@ -166,6 +211,33 @@ public class Proxy { Collections.addAll(this.status.getList(), Formatter.DARK_GREEN + "TESTTTT", "Test 2!", Formatter.BLUE + "Test numbah 3!!!!!"); } + private static void saveFile(JsonObject obj, File file) { + try { + Files.write(GSON.toJson(obj), file, Charsets.UTF_8); + } + catch(IOException e) { + Log.error(e, "Could not save %s", file); + } + } + + public void saveData() { + this.saveConfig(); + this.saveUsers(); + } + + public void saveConfig() { + JsonObject obj = new JsonObject(); + // ... + saveFile(obj, new File("vproxy.json")); + } + + public void saveUsers() { + JsonObject obj = new JsonObject(); + obj.add("users", User.toJson(this.users.values())); + // ... + saveFile(obj, new File("users.json")); + } + public void run() { this.loadConfig(); Log.info("Hosting proxy on %s:%d", this.proxyHost.isEmpty() ? "0.0.0.0" : this.proxyHost, this.proxyPort); @@ -178,7 +250,7 @@ public class Proxy { } Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { public void run() { - Proxy.this.terminateEndpoints(); + Proxy.this.endProxy(); } }, "Proxy shutdown thread")); Thread con = new Thread(new Runnable() { @@ -233,9 +305,14 @@ public class Proxy { catch(InterruptedException e) { } } - this.terminateEndpoints(); + this.endProxy(); Log.info("Proxy stopped"); } + + private void endProxy() { + this.terminateEndpoints(); + this.saveData(); + } public void addLanEndpoint(InetAddress address, int port) throws IOException { synchronized(this.endpoints) { @@ -538,7 +615,7 @@ public class Proxy { Log.error("Command '%s' not found", args[0]); return false; } - if(player != null && !player.isAdmin()) { + if(player != null && (cmd.getPermission() == null || (cmd.getPermission().isEmpty() && !player.isAdmin()) || !player.hasPermission(cmd.getPermission()))) { player.sendMessage(Formatter.DARK_RED + "You are not allowed to execute this command"); return true; } diff --git a/proxy/src/main/java/proxy/command/Command.java b/proxy/src/main/java/proxy/command/Command.java index c7e5c74..9455357 100644 --- a/proxy/src/main/java/proxy/command/Command.java +++ b/proxy/src/main/java/proxy/command/Command.java @@ -18,6 +18,9 @@ public abstract class Command { public int getRedactedLogArg(int args) { return -1; } + public String getPermission() { + return "vproxy.cmd." + this.getName(); + } protected static void sendMessage(ProxyHandler player, String msg) { if(player != null) diff --git a/proxy/src/main/java/proxy/command/CommandAdmin.java b/proxy/src/main/java/proxy/command/CommandAdmin.java index 68395dc..67c6033 100644 --- a/proxy/src/main/java/proxy/command/CommandAdmin.java +++ b/proxy/src/main/java/proxy/command/CommandAdmin.java @@ -19,6 +19,10 @@ public class CommandAdmin extends Command { public String getArgs() { return "[username]"; } + + public String getPermission() { + return ""; + } public void run(Proxy proxy, ProxyHandler player, String[] args) { if(args.length == 0) { diff --git a/proxy/src/main/java/proxy/command/CommandDelete.java b/proxy/src/main/java/proxy/command/CommandDelete.java index 7df2c86..f80e448 100644 --- a/proxy/src/main/java/proxy/command/CommandDelete.java +++ b/proxy/src/main/java/proxy/command/CommandDelete.java @@ -34,6 +34,6 @@ public class CommandDelete extends Command { } public Iterable complete(Proxy proxy, ProxyHandler player, String[] args) { - return args.length == 1 ? Collections2.filter(proxy.getUserNames(), user -> !proxy.getUser(user).isAdmin()) : null; + return args.length == 1 ? (player == null ? proxy.getUserNames() : Collections2.filter(proxy.getUserNames(), user -> !proxy.getUser(user).isAdmin())) : null; } } diff --git a/proxy/src/main/java/proxy/command/CommandPing.java b/proxy/src/main/java/proxy/command/CommandPing.java index 06c8280..1a39857 100644 --- a/proxy/src/main/java/proxy/command/CommandPing.java +++ b/proxy/src/main/java/proxy/command/CommandPing.java @@ -2,6 +2,7 @@ package proxy.command; import proxy.Proxy; import proxy.handler.ProxyHandler; +import proxy.util.Permissions; public class CommandPing extends Command { public String getName() { @@ -22,12 +23,12 @@ public class CommandPing extends Command { ProxyHandler plr = args.length < 1 ? player : proxy.getPlayer(args[0]); if(plr == null) throw new RunException("'%s' is not online", args[0]); - if(player != null && plr != player && !player.isAdmin()) + if(player != null && plr != player && !player.hasPermission(Permissions.PING_OTHERS)) throw new RunException("You are not allowed to query the latency of another player"); sendInfo(player, (plr == player ? "Your latency" : "Latency of %s") + ": %d ms", plr == player ? plr.getPing() : plr.getUsername(), plr.getPing()); } public Iterable complete(Proxy proxy, ProxyHandler player, String[] args) { - return args.length == 1 && (player == null || player.isAdmin()) ? proxy.getPlayerNames() : null; + return args.length == 1 && (player == null || player.hasPermission(Permissions.PING_OTHERS)) ? proxy.getPlayerNames() : null; } } diff --git a/proxy/src/main/java/proxy/command/CommandRevoke.java b/proxy/src/main/java/proxy/command/CommandRevoke.java index bc94f8d..6ce070a 100644 --- a/proxy/src/main/java/proxy/command/CommandRevoke.java +++ b/proxy/src/main/java/proxy/command/CommandRevoke.java @@ -1,5 +1,7 @@ package proxy.command; +import com.google.common.collect.Collections2; + import proxy.Proxy; import proxy.handler.ProxyHandler; import proxy.util.Formatter; @@ -17,10 +19,12 @@ public class CommandRevoke extends Command { public String getArgs() { return ""; } + + public String getPermission() { + return null; + } public void run(Proxy proxy, ProxyHandler player, String[] args) { - if(player != null) - throw new RunException("Only the console can revoke admin status"); if(args.length < 1) throw new RunException("Please provide a username"); User user = proxy.getUser(args[0]); @@ -33,4 +37,8 @@ public class CommandRevoke extends Command { ((ProxyHandler)user).sendMessage(Formatter.RED + "Your admin privileges were revoked"); sendInfo(player, "%s is no longer an admin", user.getUsername()); } + + public Iterable complete(Proxy proxy, ProxyHandler player, String[] args) { + return args.length == 1 ? Collections2.filter(proxy.getUserNames(), user -> proxy.getUser(user).isAdmin()) : null; + } } diff --git a/proxy/src/main/java/proxy/handler/ProxyHandler.java b/proxy/src/main/java/proxy/handler/ProxyHandler.java index 28de1c2..25ba9e6 100755 --- a/proxy/src/main/java/proxy/handler/ProxyHandler.java +++ b/proxy/src/main/java/proxy/handler/ProxyHandler.java @@ -26,6 +26,7 @@ import proxy.packet.S38PacketPlayerListItem.Action; import proxy.util.Formatter; import proxy.util.User; import proxy.util.Log; +import proxy.util.Permissions; public class ProxyHandler extends User implements Handler { public class ProxyLoginHandler implements Handler { @@ -351,7 +352,7 @@ public class ProxyHandler extends User implements Handler { this.disconnect("You are already logged in"); Log.info("%s was already logged in from another client", this.username); } - else if(!stored.isAdmin() && this.proxy.getMaximumPlayers() > 0 && this.proxy.getOnlinePlayers() >= this.proxy.getMaximumPlayers()) { + else if(!stored.hasPermission(Permissions.BYPASS_PLAYER_LIMIT) && this.proxy.getMaximumPlayers() > 0 && this.proxy.getOnlinePlayers() >= this.proxy.getMaximumPlayers()) { this.disconnect("The server is full (" + this.proxy.getOnlinePlayers() + " / " + this.proxy.getMaximumPlayers() + " players), please try again later"); } else { @@ -388,7 +389,7 @@ public class ProxyHandler extends User implements Handler { if(cmd == null) return null; List list = Lists.newArrayList(); - if(!this.isAdmin()) + if(cmd.getPermission() == null || (cmd.getPermission().isEmpty() && !this.isAdmin()) || !this.hasPermission(cmd.getPermission())) return list; String[] argv = new String[args.length - 1]; System.arraycopy(args, 1, argv, 0, argv.length); @@ -453,12 +454,10 @@ public class ProxyHandler extends User implements Handler { if(cmd.isEmpty() || cmd.indexOf(':') != -1 || this.proxy.getCommands().containsKey(cmd.substring(1).toLowerCase(Locale.US))) iter.remove(); } - if(this.admin) { - for(String cmd : this.proxy.getCommands().keySet()) { - String command = "/" + cmd; - if(this.admin && command.regionMatches(true, 0, this.completion, 0, this.completion.length())) - list.add(command); - } + for(Command cmd : this.proxy.getCommands().values()) { + String command = "/" + cmd.getName(); + if(cmd.getPermission() != null && (!cmd.getPermission().isEmpty() || this.isAdmin()) && command.regionMatches(true, 0, this.completion, 0, this.completion.length())) + list.add(command); } Collections.sort(list); } diff --git a/proxy/src/main/java/proxy/util/Formatter.java b/proxy/src/main/java/proxy/util/Formatter.java index 8ae7ca2..cf4fac9 100755 --- a/proxy/src/main/java/proxy/util/Formatter.java +++ b/proxy/src/main/java/proxy/util/Formatter.java @@ -101,6 +101,29 @@ public enum Formatter { return true; } + public static String getHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for(int z = 0; z < bytes.length; z++) { + sb.append(String.format("%02x", bytes[z])); + } + return sb.toString(); + } + + public static byte[] fromHexString(String str) { + if((str.length() & 1) == 1) + str = "0" + str; + byte[] bytes = new byte[str.length() / 2]; + try { + for(int z = 0; z < bytes.length; z++) { + bytes[z] = (byte)Integer.parseUnsignedInt(str.substring(z * 2, (z + 1) * 2), 16); + } + } + catch(NumberFormatException e) { + return null; + } + return bytes; + } + private Formatter(char code) { this.value = "\u00a7" + code; } diff --git a/proxy/src/main/java/proxy/util/Permissions.java b/proxy/src/main/java/proxy/util/Permissions.java new file mode 100644 index 0000000..b9a6169 --- /dev/null +++ b/proxy/src/main/java/proxy/util/Permissions.java @@ -0,0 +1,7 @@ +package proxy.util; + +public class Permissions { + public static final String BYPASS_PLAYER_LIMIT = "vproxy.bypass.playerlimit"; + + public static final String PING_OTHERS = "vproxy.cmd.ping.others"; +} diff --git a/proxy/src/main/java/proxy/util/User.java b/proxy/src/main/java/proxy/util/User.java index 1cfaf85..5b9f5fd 100644 --- a/proxy/src/main/java/proxy/util/User.java +++ b/proxy/src/main/java/proxy/util/User.java @@ -4,6 +4,11 @@ import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.Collection; +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; public class User { private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -37,10 +42,19 @@ public class User { return this.admin; } + public boolean hasPermission(String perm) { + return this.isAdmin(); + } + public void setAdmin(boolean admin) { this.admin = admin; } + public void setSkinTexture(String data, String signature) { + this.skinTexture = data; + this.skinSignature = signature; + } + public boolean isOnline() { return false; } @@ -49,6 +63,56 @@ public class User { this.hash = user.hash; this.salt = user.salt; this.admin = user.admin; + this.skinTexture = user.skinTexture; + this.skinSignature = user.skinSignature; + } + + private JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("username", this.username); + obj.addProperty("passwordHash", Formatter.getHexString(this.hash)); + obj.addProperty("passwordSalt", Formatter.getHexString(this.salt)); + obj.addProperty("admin", this.admin); + obj.addProperty("skinTexture", this.skinTexture); + obj.addProperty("skinSignature", this.skinSignature); + return obj; + } + + private boolean fromJson(JsonObject obj) { + if(!Formatter.isValidUser(this.username)) + return false; + this.hash = obj.has("passwordHash") ? Formatter.fromHexString(obj.get("passwordHash").getAsString()) : null; + this.salt = obj.has("passwordSalt") ? Formatter.fromHexString(obj.get("passwordSalt").getAsString()) : null; + this.admin = obj.has("admin") ? obj.get("admin").getAsBoolean() : false; + this.skinTexture = obj.has("skinTexture") ? obj.get("skinTexture").getAsString() : null; + this.skinSignature = obj.has("skinSignature") ? obj.get("skinSignature").getAsString() : null; + return this.hash != null && this.salt != null; + } + + public static JsonArray toJson(Collection users) { + JsonArray arr = new JsonArray(); + for(User user : users) { + arr.add(user.toJson()); + } + return arr; + } + + public static Collection fromJson(JsonArray arr) { + Collection users = Lists.newArrayList(); + for(JsonElement elem : arr) { + if(!elem.isJsonObject()) + continue; + JsonObject obj = elem.getAsJsonObject(); + if(!obj.has("username")) + continue; + JsonElement name = obj.get("username"); + if(!name.isJsonPrimitive()) + continue; + User user = new User(name.getAsString()); + if(user.fromJson(obj)) + users.add(user); + } + return users; } private static byte[] hashPassword(String pass, byte[] salt) {