add basic user saving

This commit is contained in:
Sen 2025-06-06 02:51:34 +02:00
parent a9b113a436
commit 5fadc725d0
Signed by: sen
GPG key ID: 3AC50A6F47D1B722
10 changed files with 203 additions and 17 deletions

View file

@ -49,11 +49,17 @@ import com.google.common.base.Charsets;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Queues; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask; import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.common.util.concurrent.ThreadFactoryBuilder; 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.Direction;
import proxy.network.Decoder; import proxy.network.Decoder;
import proxy.network.Splitter; 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()); 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 isAlive;
private volatile boolean running = true; private volatile boolean running = true;
@ -153,7 +161,44 @@ public class Proxy {
this.registerCommands(); 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() { 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 = new ServerInfo("VLoginProxy 1.8.9", 47);
this.status.setCapacity(this.maxPlayers); 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!!"); 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!!!!!"); 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() { public void run() {
this.loadConfig(); this.loadConfig();
Log.info("Hosting proxy on %s:%d", this.proxyHost.isEmpty() ? "0.0.0.0" : this.proxyHost, this.proxyPort); 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() { Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() { public void run() {
Proxy.this.terminateEndpoints(); Proxy.this.endProxy();
} }
}, "Proxy shutdown thread")); }, "Proxy shutdown thread"));
Thread con = new Thread(new Runnable() { Thread con = new Thread(new Runnable() {
@ -233,10 +305,15 @@ public class Proxy {
catch(InterruptedException e) { catch(InterruptedException e) {
} }
} }
this.terminateEndpoints(); this.endProxy();
Log.info("Proxy stopped"); Log.info("Proxy stopped");
} }
private void endProxy() {
this.terminateEndpoints();
this.saveData();
}
public void addLanEndpoint(InetAddress address, int port) throws IOException { public void addLanEndpoint(InetAddress address, int port) throws IOException {
synchronized(this.endpoints) { synchronized(this.endpoints) {
Class<? extends ServerSocketChannel> oclass; Class<? extends ServerSocketChannel> oclass;
@ -538,7 +615,7 @@ public class Proxy {
Log.error("Command '%s' not found", args[0]); Log.error("Command '%s' not found", args[0]);
return false; 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"); player.sendMessage(Formatter.DARK_RED + "You are not allowed to execute this command");
return true; return true;
} }

View file

@ -18,6 +18,9 @@ public abstract class Command {
public int getRedactedLogArg(int args) { public int getRedactedLogArg(int args) {
return -1; return -1;
} }
public String getPermission() {
return "vproxy.cmd." + this.getName();
}
protected static void sendMessage(ProxyHandler player, String msg) { protected static void sendMessage(ProxyHandler player, String msg) {
if(player != null) if(player != null)

View file

@ -20,6 +20,10 @@ public class CommandAdmin extends Command {
return "[username]"; return "[username]";
} }
public String getPermission() {
return "";
}
public void run(Proxy proxy, ProxyHandler player, String[] args) { public void run(Proxy proxy, ProxyHandler player, String[] args) {
if(args.length == 0) { if(args.length == 0) {
int cnt = 0; int cnt = 0;

View file

@ -34,6 +34,6 @@ public class CommandDelete extends Command {
} }
public Iterable<String> complete(Proxy proxy, ProxyHandler player, String[] args) { public Iterable<String> 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;
} }
} }

View file

@ -2,6 +2,7 @@ package proxy.command;
import proxy.Proxy; import proxy.Proxy;
import proxy.handler.ProxyHandler; import proxy.handler.ProxyHandler;
import proxy.util.Permissions;
public class CommandPing extends Command { public class CommandPing extends Command {
public String getName() { public String getName() {
@ -22,12 +23,12 @@ public class CommandPing extends Command {
ProxyHandler plr = args.length < 1 ? player : proxy.getPlayer(args[0]); ProxyHandler plr = args.length < 1 ? player : proxy.getPlayer(args[0]);
if(plr == null) if(plr == null)
throw new RunException("'%s' is not online", args[0]); 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"); 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()); sendInfo(player, (plr == player ? "Your latency" : "Latency of %s") + ": %d ms", plr == player ? plr.getPing() : plr.getUsername(), plr.getPing());
} }
public Iterable<String> complete(Proxy proxy, ProxyHandler player, String[] args) { public Iterable<String> 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;
} }
} }

View file

@ -1,5 +1,7 @@
package proxy.command; package proxy.command;
import com.google.common.collect.Collections2;
import proxy.Proxy; import proxy.Proxy;
import proxy.handler.ProxyHandler; import proxy.handler.ProxyHandler;
import proxy.util.Formatter; import proxy.util.Formatter;
@ -18,9 +20,11 @@ public class CommandRevoke extends Command {
return "<username>"; return "<username>";
} }
public String getPermission() {
return null;
}
public void run(Proxy proxy, ProxyHandler player, String[] args) { 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) if(args.length < 1)
throw new RunException("Please provide a username"); throw new RunException("Please provide a username");
User user = proxy.getUser(args[0]); 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"); ((ProxyHandler)user).sendMessage(Formatter.RED + "Your admin privileges were revoked");
sendInfo(player, "%s is no longer an admin", user.getUsername()); sendInfo(player, "%s is no longer an admin", user.getUsername());
} }
public Iterable<String> complete(Proxy proxy, ProxyHandler player, String[] args) {
return args.length == 1 ? Collections2.filter(proxy.getUserNames(), user -> proxy.getUser(user).isAdmin()) : null;
}
} }

View file

@ -26,6 +26,7 @@ import proxy.packet.S38PacketPlayerListItem.Action;
import proxy.util.Formatter; import proxy.util.Formatter;
import proxy.util.User; import proxy.util.User;
import proxy.util.Log; import proxy.util.Log;
import proxy.util.Permissions;
public class ProxyHandler extends User implements Handler { public class ProxyHandler extends User implements Handler {
public class ProxyLoginHandler 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"); this.disconnect("You are already logged in");
Log.info("%s was already logged in from another client", this.username); 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"); this.disconnect("The server is full (" + this.proxy.getOnlinePlayers() + " / " + this.proxy.getMaximumPlayers() + " players), please try again later");
} }
else { else {
@ -388,7 +389,7 @@ public class ProxyHandler extends User implements Handler {
if(cmd == null) if(cmd == null)
return null; return null;
List<String> list = Lists.<String>newArrayList(); List<String> list = Lists.<String>newArrayList();
if(!this.isAdmin()) if(cmd.getPermission() == null || (cmd.getPermission().isEmpty() && !this.isAdmin()) || !this.hasPermission(cmd.getPermission()))
return list; return list;
String[] argv = new String[args.length - 1]; String[] argv = new String[args.length - 1];
System.arraycopy(args, 1, argv, 0, argv.length); System.arraycopy(args, 1, argv, 0, argv.length);
@ -453,13 +454,11 @@ public class ProxyHandler extends User implements Handler {
if(cmd.isEmpty() || cmd.indexOf(':') != -1 || this.proxy.getCommands().containsKey(cmd.substring(1).toLowerCase(Locale.US))) if(cmd.isEmpty() || cmd.indexOf(':') != -1 || this.proxy.getCommands().containsKey(cmd.substring(1).toLowerCase(Locale.US)))
iter.remove(); iter.remove();
} }
if(this.admin) { for(Command cmd : this.proxy.getCommands().values()) {
for(String cmd : this.proxy.getCommands().keySet()) { String command = "/" + cmd.getName();
String command = "/" + cmd; if(cmd.getPermission() != null && (!cmd.getPermission().isEmpty() || this.isAdmin()) && command.regionMatches(true, 0, this.completion, 0, this.completion.length()))
if(this.admin && command.regionMatches(true, 0, this.completion, 0, this.completion.length()))
list.add(command); list.add(command);
} }
}
Collections.sort(list); Collections.sort(list);
} }
else { else {

View file

@ -101,6 +101,29 @@ public enum Formatter {
return true; 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) { private Formatter(char code) {
this.value = "\u00a7" + code; this.value = "\u00a7" + code;
} }

View file

@ -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";
}

View file

@ -4,6 +4,11 @@ import java.nio.charset.Charset;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; 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 { public class User {
private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final Charset UTF_8 = Charset.forName("UTF-8");
@ -37,10 +42,19 @@ public class User {
return this.admin; return this.admin;
} }
public boolean hasPermission(String perm) {
return this.isAdmin();
}
public void setAdmin(boolean admin) { public void setAdmin(boolean admin) {
this.admin = admin; this.admin = admin;
} }
public void setSkinTexture(String data, String signature) {
this.skinTexture = data;
this.skinSignature = signature;
}
public boolean isOnline() { public boolean isOnline() {
return false; return false;
} }
@ -49,6 +63,56 @@ public class User {
this.hash = user.hash; this.hash = user.hash;
this.salt = user.salt; this.salt = user.salt;
this.admin = user.admin; 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<User> users) {
JsonArray arr = new JsonArray();
for(User user : users) {
arr.add(user.toJson());
}
return arr;
}
public static Collection<User> fromJson(JsonArray arr) {
Collection<User> 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) { private static byte[] hashPassword(String pass, byte[] salt) {