From 0f47403d0220757fed189b76e2071e25b1025cb8 Mon Sep 17 00:00:00 2001 From: Runemoro Date: Wed, 3 Jun 2020 13:39:42 -0400 Subject: Split GUI code to separate module (#242) * Split into modules * Post merge compile fixes Co-authored-by: modmuss50 --- .../cuchaz/enigma/network/ClientPacketHandler.java | 29 ++ .../enigma/network/DedicatedEnigmaServer.java | 200 +++++++++++ .../java/cuchaz/enigma/network/EnigmaClient.java | 78 ++++ .../java/cuchaz/enigma/network/EnigmaServer.java | 290 +++++++++++++++ .../enigma/network/IntegratedEnigmaServer.java | 16 + .../main/java/cuchaz/enigma/network/Message.java | 393 +++++++++++++++++++++ .../cuchaz/enigma/network/ServerPacketHandler.java | 22 ++ .../enigma/network/packet/ChangeDocsC2SPacket.java | 59 ++++ .../enigma/network/packet/ChangeDocsS2CPacket.java | 44 +++ .../network/packet/ConfirmChangeC2SPacket.java | 33 ++ .../enigma/network/packet/KickS2CPacket.java | 33 ++ .../enigma/network/packet/LoginC2SPacket.java | 75 ++++ .../network/packet/MarkDeobfuscatedC2SPacket.java | 48 +++ .../network/packet/MarkDeobfuscatedS2CPacket.java | 40 +++ .../enigma/network/packet/MessageC2SPacket.java | 39 ++ .../enigma/network/packet/MessageS2CPacket.java | 36 ++ .../java/cuchaz/enigma/network/packet/Packet.java | 15 + .../cuchaz/enigma/network/packet/PacketHelper.java | 135 +++++++ .../enigma/network/packet/PacketRegistry.java | 64 ++++ .../network/packet/RemoveMappingC2SPacket.java | 55 +++ .../network/packet/RemoveMappingS2CPacket.java | 40 +++ .../enigma/network/packet/RenameC2SPacket.java | 64 ++++ .../enigma/network/packet/RenameS2CPacket.java | 48 +++ .../network/packet/SyncMappingsS2CPacket.java | 88 +++++ .../enigma/network/packet/UserListS2CPacket.java | 44 +++ 25 files changed, 1988 insertions(+) create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/EnigmaClient.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/EnigmaServer.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/Message.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/Packet.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java create mode 100644 enigma-server/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java (limited to 'enigma-server/src/main/java') diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java b/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java new file mode 100644 index 0000000..720744b --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java @@ -0,0 +1,29 @@ +package cuchaz.enigma.network; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.network.packet.Packet; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.util.List; + +public interface ClientPacketHandler { + void openMappings(EntryTree mappings); + + void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree, boolean jumpToReference); + + void removeMapping(EntryReference, Entry> reference, boolean jumpToReference); + + void changeDocs(EntryReference, Entry> reference, String updatedDocs, boolean jumpToReference); + + void markAsDeobfuscated(EntryReference, Entry> reference, boolean jumpToReference); + + void disconnectIfConnected(String reason); + + void sendPacket(Packet packet); + + void addMessage(Message message); + + void updateUserList(List users); +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java b/enigma-server/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java new file mode 100644 index 0000000..924302f --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java @@ -0,0 +1,200 @@ +package cuchaz.enigma.network; + +import com.google.common.io.MoreFiles; +import cuchaz.enigma.*; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.serde.MappingFormat; +import cuchaz.enigma.utils.Utils; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import joptsimple.ValueConverter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +public class DedicatedEnigmaServer extends EnigmaServer { + + private final EnigmaProfile profile; + private final MappingFormat mappingFormat; + private final Path mappingsFile; + private final PrintWriter log; + private BlockingQueue tasks = new LinkedBlockingDeque<>(); + + public DedicatedEnigmaServer( + byte[] jarChecksum, + char[] password, + EnigmaProfile profile, + MappingFormat mappingFormat, + Path mappingsFile, + PrintWriter log, + EntryRemapper mappings, + int port + ) { + super(jarChecksum, password, mappings, port); + this.profile = profile; + this.mappingFormat = mappingFormat; + this.mappingsFile = mappingsFile; + this.log = log; + } + + @Override + protected void runOnThread(Runnable task) { + tasks.add(task); + } + + @Override + public void log(String message) { + super.log(message); + log.println(message); + } + + public static void main(String[] args) { + OptionParser parser = new OptionParser(); + + OptionSpec jarOpt = parser.accepts("jar", "Jar file to open at startup") + .withRequiredArg() + .required() + .withValuesConvertedBy(PathConverter.INSTANCE); + + OptionSpec mappingsOpt = parser.accepts("mappings", "Mappings file to open at startup") + .withRequiredArg() + .required() + .withValuesConvertedBy(PathConverter.INSTANCE); + + OptionSpec profileOpt = parser.accepts("profile", "Profile json to apply at startup") + .withRequiredArg() + .withValuesConvertedBy(PathConverter.INSTANCE); + + OptionSpec portOpt = parser.accepts("port", "Port to run the server on") + .withOptionalArg() + .ofType(Integer.class) + .defaultsTo(EnigmaServer.DEFAULT_PORT); + + OptionSpec passwordOpt = parser.accepts("password", "The password to join the server") + .withRequiredArg() + .defaultsTo(""); + + OptionSpec logFileOpt = parser.accepts("log", "The log file to write to") + .withRequiredArg() + .withValuesConvertedBy(PathConverter.INSTANCE) + .defaultsTo(Paths.get("log.txt")); + + OptionSet parsedArgs = parser.parse(args); + Path jar = parsedArgs.valueOf(jarOpt); + Path mappingsFile = parsedArgs.valueOf(mappingsOpt); + Path profileFile = parsedArgs.valueOf(profileOpt); + int port = parsedArgs.valueOf(portOpt); + char[] password = parsedArgs.valueOf(passwordOpt).toCharArray(); + if (password.length > EnigmaServer.MAX_PASSWORD_LENGTH) { + System.err.println("Password too long, must be at most " + EnigmaServer.MAX_PASSWORD_LENGTH + " characters"); + System.exit(1); + } + Path logFile = parsedArgs.valueOf(logFileOpt); + + System.out.println("Starting Enigma server"); + DedicatedEnigmaServer server; + try { + byte[] checksum = Utils.zipSha1(parsedArgs.valueOf(jarOpt)); + + EnigmaProfile profile = EnigmaProfile.read(profileFile); + Enigma enigma = Enigma.builder().setProfile(profile).build(); + System.out.println("Indexing Jar..."); + EnigmaProject project = enigma.openJar(jar, ProgressListener.none()); + + MappingFormat mappingFormat = MappingFormat.ENIGMA_DIRECTORY; + EntryRemapper mappings; + if (!Files.exists(mappingsFile)) { + mappings = EntryRemapper.empty(project.getJarIndex()); + } else { + System.out.println("Reading mappings..."); + if (Files.isDirectory(mappingsFile)) { + mappingFormat = MappingFormat.ENIGMA_DIRECTORY; + } else if ("zip".equalsIgnoreCase(MoreFiles.getFileExtension(mappingsFile))) { + mappingFormat = MappingFormat.ENIGMA_ZIP; + } else { + mappingFormat = MappingFormat.ENIGMA_FILE; + } + mappings = EntryRemapper.mapped(project.getJarIndex(), mappingFormat.read(mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters())); + } + + PrintWriter log = new PrintWriter(Files.newBufferedWriter(logFile)); + + server = new DedicatedEnigmaServer(checksum, password, profile, mappingFormat, mappingsFile, log, mappings, port); + server.start(); + System.out.println("Server started"); + } catch (IOException | MappingParseException e) { + System.err.println("Error starting server!"); + e.printStackTrace(); + System.exit(1); + return; + } + + // noinspection RedundantSuppression + // noinspection Convert2MethodRef - javac 8 bug + Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> server.runOnThread(() -> server.saveMappings()), 0, 1, TimeUnit.MINUTES); + Runtime.getRuntime().addShutdownHook(new Thread(server::saveMappings)); + + while (true) { + try { + server.tasks.take().run(); + } catch (InterruptedException e) { + break; + } + } + } + + @Override + public synchronized void stop() { + super.stop(); + System.exit(0); + } + + private void saveMappings() { + mappingFormat.write(getMappings().getObfToDeobf(), getMappings().takeMappingDelta(), mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters()); + log.flush(); + } + + public static class PathConverter implements ValueConverter { + public static final ValueConverter INSTANCE = new PathConverter(); + + PathConverter() { + } + + @Override + public Path convert(String path) { + // expand ~ to the home dir + if (path.startsWith("~")) { + // get the home dir + Path dirHome = Paths.get(System.getProperty("user.home")); + + // is the path just ~/ or is it ~user/ ? + if (path.startsWith("~/")) { + return dirHome.resolve(path.substring(2)); + } else { + return dirHome.getParent().resolve(path.substring(1)); + } + } + + return Paths.get(path); + } + + @Override + public Class valueType() { + return Path.class; + } + + @Override + public String valuePattern() { + return "path"; + } + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaClient.java b/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaClient.java new file mode 100644 index 0000000..71bd011 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaClient.java @@ -0,0 +1,78 @@ +package cuchaz.enigma.network; + +import cuchaz.enigma.network.packet.Packet; +import cuchaz.enigma.network.packet.PacketRegistry; + +import javax.swing.*; +import java.io.*; +import java.net.Socket; +import java.net.SocketException; + +public class EnigmaClient { + + private final ClientPacketHandler controller; + + private final String ip; + private final int port; + private Socket socket; + private DataOutput output; + + public EnigmaClient(ClientPacketHandler controller, String ip, int port) { + this.controller = controller; + this.ip = ip; + this.port = port; + } + + public void connect() throws IOException { + socket = new Socket(ip, port); + output = new DataOutputStream(socket.getOutputStream()); + Thread thread = new Thread(() -> { + try { + DataInput input = new DataInputStream(socket.getInputStream()); + while (true) { + int packetId; + try { + packetId = input.readUnsignedByte(); + } catch (EOFException | SocketException e) { + break; + } + Packet packet = PacketRegistry.createS2CPacket(packetId); + if (packet == null) { + throw new IOException("Received invalid packet id " + packetId); + } + packet.read(input); + SwingUtilities.invokeLater(() -> packet.handle(controller)); + } + } catch (IOException e) { + controller.disconnectIfConnected(e.toString()); + return; + } + controller.disconnectIfConnected("Disconnected"); + }); + thread.setName("Client I/O thread"); + thread.setDaemon(true); + thread.start(); + } + + public synchronized void disconnect() { + if (socket != null && !socket.isClosed()) { + try { + socket.close(); + } catch (IOException e1) { + System.err.println("Failed to close socket"); + e1.printStackTrace(); + } + } + } + + + public void sendPacket(Packet packet) { + try { + output.writeByte(PacketRegistry.getC2SId(packet)); + packet.write(output); + } catch (IOException e) { + controller.disconnectIfConnected(e.toString()); + } + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaServer.java b/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaServer.java new file mode 100644 index 0000000..6027a6b --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaServer.java @@ -0,0 +1,290 @@ +package cuchaz.enigma.network; + +import cuchaz.enigma.network.packet.KickS2CPacket; +import cuchaz.enigma.network.packet.MessageS2CPacket; +import cuchaz.enigma.network.packet.Packet; +import cuchaz.enigma.network.packet.PacketRegistry; +import cuchaz.enigma.network.packet.RemoveMappingS2CPacket; +import cuchaz.enigma.network.packet.RenameS2CPacket; +import cuchaz.enigma.network.packet.UserListS2CPacket; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +public abstract class EnigmaServer { + + // https://discordapp.com/channels/507304429255393322/566418023372816394/700292322918793347 + public static final int DEFAULT_PORT = 34712; + public static final int PROTOCOL_VERSION = 0; + public static final String OWNER_USERNAME = "Owner"; + public static final int CHECKSUM_SIZE = 20; + public static final int MAX_PASSWORD_LENGTH = 255; // length is written as a byte in the login packet + + private final int port; + private ServerSocket socket; + private List clients = new CopyOnWriteArrayList<>(); + private Map usernames = new HashMap<>(); + private Set unapprovedClients = new HashSet<>(); + + private final byte[] jarChecksum; + private final char[] password; + + public static final int DUMMY_SYNC_ID = 0; + private final EntryRemapper mappings; + private Map, Integer> syncIds = new HashMap<>(); + private Map> inverseSyncIds = new HashMap<>(); + private Map> clientsNeedingConfirmation = new HashMap<>(); + private int nextSyncId = DUMMY_SYNC_ID + 1; + + private static int nextIoId = 0; + + public EnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) { + this.jarChecksum = jarChecksum; + this.password = password; + this.mappings = mappings; + this.port = port; + } + + public void start() throws IOException { + socket = new ServerSocket(port); + log("Server started on " + socket.getInetAddress() + ":" + port); + Thread thread = new Thread(() -> { + try { + while (!socket.isClosed()) { + acceptClient(); + } + } catch (SocketException e) { + System.out.println("Server closed"); + } catch (IOException e) { + e.printStackTrace(); + } + }); + thread.setName("Server client listener"); + thread.setDaemon(true); + thread.start(); + } + + private void acceptClient() throws IOException { + Socket client = socket.accept(); + clients.add(client); + Thread thread = new Thread(() -> { + try { + DataInput input = new DataInputStream(client.getInputStream()); + while (true) { + int packetId; + try { + packetId = input.readUnsignedByte(); + } catch (EOFException | SocketException e) { + break; + } + Packet packet = PacketRegistry.createC2SPacket(packetId); + if (packet == null) { + throw new IOException("Received invalid packet id " + packetId); + } + packet.read(input); + runOnThread(() -> packet.handle(new ServerPacketHandler(client, this))); + } + } catch (IOException e) { + kick(client, e.toString()); + e.printStackTrace(); + return; + } + kick(client, "disconnect.disconnected"); + }); + thread.setName("Server I/O thread #" + (nextIoId++)); + thread.setDaemon(true); + thread.start(); + } + + public void stop() { + runOnThread(() -> { + if (socket != null && !socket.isClosed()) { + for (Socket client : clients) { + kick(client, "disconnect.server_closed"); + } + try { + socket.close(); + } catch (IOException e) { + System.err.println("Failed to close server socket"); + e.printStackTrace(); + } + } + }); + } + + public void kick(Socket client, String reason) { + if (!clients.remove(client)) return; + + sendPacket(client, new KickS2CPacket(reason)); + + clientsNeedingConfirmation.values().removeIf(list -> { + list.remove(client); + return list.isEmpty(); + }); + String username = usernames.remove(client); + try { + client.close(); + } catch (IOException e) { + System.err.println("Failed to close server client socket"); + e.printStackTrace(); + } + + if (username != null) { + System.out.println("Kicked " + username + " because " + reason); + sendMessage(Message.disconnect(username)); + } + sendUsernamePacket(); + } + + public boolean isUsernameTaken(String username) { + return usernames.containsValue(username); + } + + public void setUsername(Socket client, String username) { + usernames.put(client, username); + sendUsernamePacket(); + } + + private void sendUsernamePacket() { + List usernames = new ArrayList<>(this.usernames.values()); + Collections.sort(usernames); + sendToAll(new UserListS2CPacket(usernames)); + } + + public String getUsername(Socket client) { + return usernames.get(client); + } + + public void sendPacket(Socket client, Packet packet) { + if (!client.isClosed()) { + int packetId = PacketRegistry.getS2CId(packet); + try { + DataOutput output = new DataOutputStream(client.getOutputStream()); + output.writeByte(packetId); + packet.write(output); + } catch (IOException e) { + if (!(packet instanceof KickS2CPacket)) { + kick(client, e.toString()); + e.printStackTrace(); + } + } + } + } + + public void sendToAll(Packet packet) { + for (Socket client : clients) { + sendPacket(client, packet); + } + } + + public void sendToAllExcept(Socket excluded, Packet packet) { + for (Socket client : clients) { + if (client != excluded) { + sendPacket(client, packet); + } + } + } + + public boolean canModifyEntry(Socket client, Entry entry) { + if (unapprovedClients.contains(client)) { + return false; + } + + Integer syncId = syncIds.get(entry); + if (syncId == null) { + return true; + } + Set clients = clientsNeedingConfirmation.get(syncId); + return clients == null || !clients.contains(client); + } + + public int lockEntry(Socket exception, Entry entry) { + int syncId = nextSyncId; + nextSyncId++; + // sync id is sent as an unsigned short, can't have more than 65536 + if (nextSyncId == 65536) { + nextSyncId = DUMMY_SYNC_ID + 1; + } + Integer oldSyncId = syncIds.get(entry); + if (oldSyncId != null) { + clientsNeedingConfirmation.remove(oldSyncId); + } + syncIds.put(entry, syncId); + inverseSyncIds.put(syncId, entry); + Set clients = new HashSet<>(this.clients); + clients.remove(exception); + clientsNeedingConfirmation.put(syncId, clients); + return syncId; + } + + public void confirmChange(Socket client, int syncId) { + if (usernames.containsKey(client)) { + unapprovedClients.remove(client); + } + + Set clients = clientsNeedingConfirmation.get(syncId); + if (clients != null) { + clients.remove(client); + if (clients.isEmpty()) { + clientsNeedingConfirmation.remove(syncId); + syncIds.remove(inverseSyncIds.remove(syncId)); + } + } + } + + public void sendCorrectMapping(Socket client, Entry entry, boolean refreshClassTree) { + EntryMapping oldMapping = mappings.getDeobfMapping(entry); + String oldName = oldMapping == null ? null : oldMapping.getTargetName(); + if (oldName == null) { + sendPacket(client, new RemoveMappingS2CPacket(DUMMY_SYNC_ID, entry)); + } else { + sendPacket(client, new RenameS2CPacket(0, entry, oldName, refreshClassTree)); + } + } + + protected abstract void runOnThread(Runnable task); + + public void log(String message) { + System.out.println(message); + } + + protected boolean isRunning() { + return !socket.isClosed(); + } + + public byte[] getJarChecksum() { + return jarChecksum; + } + + public char[] getPassword() { + return password; + } + + public EntryRemapper getMappings() { + return mappings; + } + + public void sendMessage(Message message) { + log(String.format("[MSG] %s", message.translate())); + sendToAll(new MessageS2CPacket(message)); + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java b/enigma-server/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java new file mode 100644 index 0000000..21c6825 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java @@ -0,0 +1,16 @@ +package cuchaz.enigma.network; + +import cuchaz.enigma.translation.mapping.EntryRemapper; + +import javax.swing.*; + +public class IntegratedEnigmaServer extends EnigmaServer { + public IntegratedEnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) { + super(jarChecksum, password, mappings, port); + } + + @Override + protected void runOnThread(Runnable task) { + SwingUtilities.invokeLater(task); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/Message.java b/enigma-server/src/main/java/cuchaz/enigma/network/Message.java new file mode 100644 index 0000000..c157838 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/Message.java @@ -0,0 +1,393 @@ +package cuchaz.enigma.network; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Objects; + +import cuchaz.enigma.network.packet.PacketHelper; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.I18n; + +public abstract class Message { + + public final String user; + + public static Chat chat(String user, String message) { + return new Chat(user, message); + } + + public static Connect connect(String user) { + return new Connect(user); + } + + public static Disconnect disconnect(String user) { + return new Disconnect(user); + } + + public static EditDocs editDocs(String user, Entry entry) { + return new EditDocs(user, entry); + } + + public static MarkDeobf markDeobf(String user, Entry entry) { + return new MarkDeobf(user, entry); + } + + public static RemoveMapping removeMapping(String user, Entry entry) { + return new RemoveMapping(user, entry); + } + + public static Rename rename(String user, Entry entry, String newName) { + return new Rename(user, entry, newName); + } + + public abstract String translate(); + + public abstract Type getType(); + + public static Message read(DataInput input) throws IOException { + byte typeId = input.readByte(); + if (typeId < 0 || typeId >= Type.values().length) { + throw new IOException(String.format("Invalid message type ID %d", typeId)); + } + Type type = Type.values()[typeId]; + String user = input.readUTF(); + switch (type) { + case CHAT: + String message = input.readUTF(); + return chat(user, message); + case CONNECT: + return connect(user); + case DISCONNECT: + return disconnect(user); + case EDIT_DOCS: + Entry entry = PacketHelper.readEntry(input); + return editDocs(user, entry); + case MARK_DEOBF: + entry = PacketHelper.readEntry(input); + return markDeobf(user, entry); + case REMOVE_MAPPING: + entry = PacketHelper.readEntry(input); + return removeMapping(user, entry); + case RENAME: + entry = PacketHelper.readEntry(input); + String newName = input.readUTF(); + return rename(user, entry, newName); + default: + throw new IllegalStateException("unreachable"); + } + } + + public void write(DataOutput output) throws IOException { + output.writeByte(getType().ordinal()); + PacketHelper.writeString(output, user); + } + + private Message(String user) { + this.user = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Message message = (Message) o; + return Objects.equals(user, message.user); + } + + @Override + public int hashCode() { + return Objects.hash(user); + } + + public enum Type { + CHAT, + CONNECT, + DISCONNECT, + EDIT_DOCS, + MARK_DEOBF, + REMOVE_MAPPING, + RENAME, + } + + public static final class Chat extends Message { + + public final String message; + + private Chat(String user, String message) { + super(user); + this.message = message; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeString(output, message); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.chat.text"), user, message); + } + + @Override + public Type getType() { + return Type.CHAT; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + Chat chat = (Chat) o; + return Objects.equals(message, chat.message); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), message); + } + + @Override + public String toString() { + return String.format("Message.Chat { user: '%s', message: '%s' }", user, message); + } + + } + + public static final class Connect extends Message { + + private Connect(String user) { + super(user); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.connect.text"), user); + } + + @Override + public Type getType() { + return Type.CONNECT; + } + + @Override + public String toString() { + return String.format("Message.Connect { user: '%s' }", user); + } + + } + + public static final class Disconnect extends Message { + + private Disconnect(String user) { + super(user); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.disconnect.text"), user); + } + + @Override + public Type getType() { + return Type.DISCONNECT; + } + + @Override + public String toString() { + return String.format("Message.Disconnect { user: '%s' }", user); + } + + } + + public static final class EditDocs extends Message { + + public final Entry entry; + + private EditDocs(String user, Entry entry) { + super(user); + this.entry = entry; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.edit_docs.text"), user, entry); + } + + @Override + public Type getType() { + return Type.EDIT_DOCS; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + EditDocs editDocs = (EditDocs) o; + return Objects.equals(entry, editDocs.entry); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry); + } + + @Override + public String toString() { + return String.format("Message.EditDocs { user: '%s', entry: %s }", user, entry); + } + + } + + public static final class MarkDeobf extends Message { + + public final Entry entry; + + private MarkDeobf(String user, Entry entry) { + super(user); + this.entry = entry; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.mark_deobf.text"), user, entry); + } + + @Override + public Type getType() { + return Type.MARK_DEOBF; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + MarkDeobf markDeobf = (MarkDeobf) o; + return Objects.equals(entry, markDeobf.entry); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry); + } + + @Override + public String toString() { + return String.format("Message.MarkDeobf { user: '%s', entry: %s }", user, entry); + } + + } + + public static final class RemoveMapping extends Message { + + public final Entry entry; + + private RemoveMapping(String user, Entry entry) { + super(user); + this.entry = entry; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.remove_mapping.text"), user, entry); + } + + @Override + public Type getType() { + return Type.REMOVE_MAPPING; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + RemoveMapping that = (RemoveMapping) o; + return Objects.equals(entry, that.entry); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry); + } + + @Override + public String toString() { + return String.format("Message.RemoveMapping { user: '%s', entry: %s }", user, entry); + } + + } + + public static final class Rename extends Message { + + public final Entry entry; + public final String newName; + + private Rename(String user, Entry entry, String newName) { + super(user); + this.entry = entry; + this.newName = newName; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newName); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.rename.text"), user, entry, newName); + } + + @Override + public Type getType() { + return Type.RENAME; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + Rename rename = (Rename) o; + return Objects.equals(entry, rename.entry) && + Objects.equals(newName, rename.newName); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry, newName); + } + + @Override + public String toString() { + return String.format("Message.Rename { user: '%s', entry: %s, newName: '%s' }", user, entry, newName); + } + + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java b/enigma-server/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java new file mode 100644 index 0000000..8618553 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java @@ -0,0 +1,22 @@ +package cuchaz.enigma.network; + +import java.net.Socket; + +public class ServerPacketHandler { + + private final Socket client; + private final EnigmaServer server; + + public ServerPacketHandler(Socket client, EnigmaServer server) { + this.client = client; + this.server = server; + } + + public Socket getClient() { + return client; + } + + public EnigmaServer getServer() { + return server; + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java new file mode 100644 index 0000000..1b52cf1 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java @@ -0,0 +1,59 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.network.Message; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.Utils; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class ChangeDocsC2SPacket implements Packet { + private Entry entry; + private String newDocs; + + ChangeDocsC2SPacket() { + } + + public ChangeDocsC2SPacket(Entry entry, String newDocs) { + this.entry = entry; + this.newDocs = newDocs; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + this.newDocs = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newDocs); + } + + @Override + public void handle(ServerPacketHandler handler) { + EntryMapping mapping = handler.getServer().getMappings().getDeobfMapping(entry); + + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + if (!valid) { + String oldDocs = mapping == null ? null : mapping.getJavadoc(); + handler.getServer().sendPacket(handler.getClient(), new ChangeDocsS2CPacket(EnigmaServer.DUMMY_SYNC_ID, entry, oldDocs == null ? "" : oldDocs)); + return; + } + + if (mapping == null) { + mapping = new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName()); + } + handler.getServer().getMappings().mapFromObf(entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs)); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new ChangeDocsS2CPacket(syncId, entry, newDocs)); + handler.getServer().sendMessage(Message.editDocs(handler.getServer().getUsername(handler.getClient()), entry)); + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java new file mode 100644 index 0000000..12a3025 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java @@ -0,0 +1,44 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class ChangeDocsS2CPacket implements Packet { + private int syncId; + private Entry entry; + private String newDocs; + + ChangeDocsS2CPacket() { + } + + public ChangeDocsS2CPacket(int syncId, Entry entry, String newDocs) { + this.syncId = syncId; + this.entry = entry; + this.newDocs = newDocs; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + this.newDocs = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newDocs); + } + + @Override + public void handle(ClientPacketHandler controller) { + controller.changeDocs(new EntryReference<>(entry, entry.getName()), newDocs, false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java new file mode 100644 index 0000000..78ef964 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java @@ -0,0 +1,33 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class ConfirmChangeC2SPacket implements Packet { + private int syncId; + + ConfirmChangeC2SPacket() { + } + + public ConfirmChangeC2SPacket(int syncId) { + this.syncId = syncId; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + } + + @Override + public void handle(ServerPacketHandler handler) { + handler.getServer().confirmChange(handler.getClient(), syncId); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java new file mode 100644 index 0000000..9a112a8 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java @@ -0,0 +1,33 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ClientPacketHandler; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class KickS2CPacket implements Packet { + private String reason; + + KickS2CPacket() { + } + + public KickS2CPacket(String reason) { + this.reason = reason; + } + + @Override + public void read(DataInput input) throws IOException { + this.reason = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeString(output, reason); + } + + @Override + public void handle(ClientPacketHandler controller) { + controller.disconnectIfConnected(reason); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java new file mode 100644 index 0000000..da0f44a --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java @@ -0,0 +1,75 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.network.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Arrays; + +public class LoginC2SPacket implements Packet { + private byte[] jarChecksum; + private char[] password; + private String username; + + LoginC2SPacket() { + } + + public LoginC2SPacket(byte[] jarChecksum, char[] password, String username) { + this.jarChecksum = jarChecksum; + this.password = password; + this.username = username; + } + + @Override + public void read(DataInput input) throws IOException { + if (input.readUnsignedShort() != EnigmaServer.PROTOCOL_VERSION) { + throw new IOException("Mismatching protocol"); + } + this.jarChecksum = new byte[EnigmaServer.CHECKSUM_SIZE]; + input.readFully(jarChecksum); + this.password = new char[input.readUnsignedByte()]; + for (int i = 0; i < password.length; i++) { + password[i] = input.readChar(); + } + this.username = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(EnigmaServer.PROTOCOL_VERSION); + output.write(jarChecksum); + output.writeByte(password.length); + for (char c : password) { + output.writeChar(c); + } + PacketHelper.writeString(output, username); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean usernameTaken = handler.getServer().isUsernameTaken(username); + handler.getServer().setUsername(handler.getClient(), username); + handler.getServer().log(username + " logged in with IP " + handler.getClient().getInetAddress().toString() + ":" + handler.getClient().getPort()); + + if (!Arrays.equals(password, handler.getServer().getPassword())) { + handler.getServer().kick(handler.getClient(), "disconnect.wrong_password"); + return; + } + + if (usernameTaken) { + handler.getServer().kick(handler.getClient(), "disconnect.username_taken"); + return; + } + + if (!Arrays.equals(jarChecksum, handler.getServer().getJarChecksum())) { + handler.getServer().kick(handler.getClient(), "disconnect.wrong_jar"); + return; + } + + handler.getServer().sendPacket(handler.getClient(), new SyncMappingsS2CPacket(handler.getServer().getMappings().getObfToDeobf())); + handler.getServer().sendMessage(Message.connect(username)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java new file mode 100644 index 0000000..a41c620 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java @@ -0,0 +1,48 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.network.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class MarkDeobfuscatedC2SPacket implements Packet { + private Entry entry; + + MarkDeobfuscatedC2SPacket() { + } + + public MarkDeobfuscatedC2SPacket(Entry entry) { + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + if (!valid) { + handler.getServer().sendCorrectMapping(handler.getClient(), entry, true); + return; + } + + handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName())); + handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " marked " + entry + " as deobfuscated"); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new MarkDeobfuscatedS2CPacket(syncId, entry)); + handler.getServer().sendMessage(Message.markDeobf(handler.getServer().getUsername(handler.getClient()), entry)); + + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java new file mode 100644 index 0000000..7504430 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java @@ -0,0 +1,40 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class MarkDeobfuscatedS2CPacket implements Packet { + private int syncId; + private Entry entry; + + MarkDeobfuscatedS2CPacket() { + } + + public MarkDeobfuscatedS2CPacket(int syncId, Entry entry) { + this.syncId = syncId; + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(ClientPacketHandler controller) { + controller.markAsDeobfuscated(new EntryReference<>(entry, entry.getName()), false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java new file mode 100644 index 0000000..3bc09e7 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java @@ -0,0 +1,39 @@ +package cuchaz.enigma.network.packet; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.network.Message; + +public class MessageC2SPacket implements Packet { + + private String message; + + MessageC2SPacket() { + } + + public MessageC2SPacket(String message) { + this.message = message; + } + + @Override + public void read(DataInput input) throws IOException { + message = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeString(output, message); + } + + @Override + public void handle(ServerPacketHandler handler) { + String message = this.message.trim(); + if (!message.isEmpty()) { + handler.getServer().sendMessage(Message.chat(handler.getServer().getUsername(handler.getClient()), message)); + } + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java new file mode 100644 index 0000000..2b07968 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java @@ -0,0 +1,36 @@ +package cuchaz.enigma.network.packet; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.network.Message; + +public class MessageS2CPacket implements Packet { + + private Message message; + + MessageS2CPacket() { + } + + public MessageS2CPacket(Message message) { + this.message = message; + } + + @Override + public void read(DataInput input) throws IOException { + message = Message.read(input); + } + + @Override + public void write(DataOutput output) throws IOException { + message.write(output); + } + + @Override + public void handle(ClientPacketHandler handler) { + handler.addMessage(message); + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/Packet.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/Packet.java new file mode 100644 index 0000000..2f16dfb --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/Packet.java @@ -0,0 +1,15 @@ +package cuchaz.enigma.network.packet; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public interface Packet { + + void read(DataInput input) throws IOException; + + void write(DataOutput output) throws IOException; + + void handle(H handler); + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java new file mode 100644 index 0000000..464606e --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java @@ -0,0 +1,135 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class PacketHelper { + + private static final int ENTRY_CLASS = 0, ENTRY_FIELD = 1, ENTRY_METHOD = 2, ENTRY_LOCAL_VAR = 3; + private static final int MAX_STRING_LENGTH = 65535; + + public static Entry readEntry(DataInput input) throws IOException { + return readEntry(input, null, true); + } + + public static Entry readEntry(DataInput input, Entry parent, boolean includeParent) throws IOException { + int type = input.readUnsignedByte(); + + if (includeParent && input.readBoolean()) { + parent = readEntry(input, null, true); + } + + String name = readString(input); + + String javadocs = null; + if (input.readBoolean()) { + javadocs = readString(input); + } + + switch (type) { + case ENTRY_CLASS: { + if (parent != null && !(parent instanceof ClassEntry)) { + throw new IOException("Class requires class parent"); + } + return new ClassEntry((ClassEntry) parent, name, javadocs); + } + case ENTRY_FIELD: { + if (!(parent instanceof ClassEntry)) { + throw new IOException("Field requires class parent"); + } + TypeDescriptor desc = new TypeDescriptor(readString(input)); + return new FieldEntry((ClassEntry) parent, name, desc, javadocs); + } + case ENTRY_METHOD: { + if (!(parent instanceof ClassEntry)) { + throw new IOException("Method requires class parent"); + } + MethodDescriptor desc = new MethodDescriptor(readString(input)); + return new MethodEntry((ClassEntry) parent, name, desc, javadocs); + } + case ENTRY_LOCAL_VAR: { + if (!(parent instanceof MethodEntry)) { + throw new IOException("Local variable requires method parent"); + } + int index = input.readUnsignedShort(); + boolean parameter = input.readBoolean(); + return new LocalVariableEntry((MethodEntry) parent, index, name, parameter, javadocs); + } + default: throw new IOException("Received unknown entry type " + type); + } + } + + public static void writeEntry(DataOutput output, Entry entry) throws IOException { + writeEntry(output, entry, true); + } + + public static void writeEntry(DataOutput output, Entry entry, boolean includeParent) throws IOException { + // type + if (entry instanceof ClassEntry) { + output.writeByte(ENTRY_CLASS); + } else if (entry instanceof FieldEntry) { + output.writeByte(ENTRY_FIELD); + } else if (entry instanceof MethodEntry) { + output.writeByte(ENTRY_METHOD); + } else if (entry instanceof LocalVariableEntry) { + output.writeByte(ENTRY_LOCAL_VAR); + } else { + throw new IOException("Don't know how to serialize entry of type " + entry.getClass().getSimpleName()); + } + + // parent + if (includeParent) { + output.writeBoolean(entry.getParent() != null); + if (entry.getParent() != null) { + writeEntry(output, entry.getParent(), true); + } + } + + // name + writeString(output, entry.getName()); + + // javadocs + output.writeBoolean(entry.getJavadocs() != null); + if (entry.getJavadocs() != null) { + writeString(output, entry.getJavadocs()); + } + + // type-specific stuff + if (entry instanceof FieldEntry) { + writeString(output, ((FieldEntry) entry).getDesc().toString()); + } else if (entry instanceof MethodEntry) { + writeString(output, ((MethodEntry) entry).getDesc().toString()); + } else if (entry instanceof LocalVariableEntry) { + LocalVariableEntry localVar = (LocalVariableEntry) entry; + output.writeShort(localVar.getIndex()); + output.writeBoolean(localVar.isArgument()); + } + } + + public static String readString(DataInput input) throws IOException { + int length = input.readUnsignedShort(); + byte[] bytes = new byte[length]; + input.readFully(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + public static void writeString(DataOutput output, String str) throws IOException { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + if (bytes.length > MAX_STRING_LENGTH) { + throw new IOException("String too long, was " + bytes.length + " bytes, max " + MAX_STRING_LENGTH + " allowed"); + } + output.writeShort(bytes.length); + output.write(bytes); + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java new file mode 100644 index 0000000..3b8af81 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java @@ -0,0 +1,64 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.network.ServerPacketHandler; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class PacketRegistry { + + private static final Map>, Integer> c2sPacketIds = new HashMap<>(); + private static final Map>> c2sPacketCreators = new HashMap<>(); + private static final Map>, Integer> s2cPacketIds = new HashMap<>(); + private static final Map>> s2cPacketCreators = new HashMap<>(); + + private static > void registerC2S(int id, Class clazz, Supplier creator) { + c2sPacketIds.put(clazz, id); + c2sPacketCreators.put(id, creator); + } + + private static > void registerS2C(int id, Class clazz, Supplier creator) { + s2cPacketIds.put(clazz, id); + s2cPacketCreators.put(id, creator); + } + + static { + registerC2S(0, LoginC2SPacket.class, LoginC2SPacket::new); + registerC2S(1, ConfirmChangeC2SPacket.class, ConfirmChangeC2SPacket::new); + registerC2S(2, RenameC2SPacket.class, RenameC2SPacket::new); + registerC2S(3, RemoveMappingC2SPacket.class, RemoveMappingC2SPacket::new); + registerC2S(4, ChangeDocsC2SPacket.class, ChangeDocsC2SPacket::new); + registerC2S(5, MarkDeobfuscatedC2SPacket.class, MarkDeobfuscatedC2SPacket::new); + registerC2S(6, MessageC2SPacket.class, MessageC2SPacket::new); + + registerS2C(0, KickS2CPacket.class, KickS2CPacket::new); + registerS2C(1, SyncMappingsS2CPacket.class, SyncMappingsS2CPacket::new); + registerS2C(2, RenameS2CPacket.class, RenameS2CPacket::new); + registerS2C(3, RemoveMappingS2CPacket.class, RemoveMappingS2CPacket::new); + registerS2C(4, ChangeDocsS2CPacket.class, ChangeDocsS2CPacket::new); + registerS2C(5, MarkDeobfuscatedS2CPacket.class, MarkDeobfuscatedS2CPacket::new); + registerS2C(6, MessageS2CPacket.class, MessageS2CPacket::new); + registerS2C(7, UserListS2CPacket.class, UserListS2CPacket::new); + } + + public static int getC2SId(Packet packet) { + return c2sPacketIds.get(packet.getClass()); + } + + public static Packet createC2SPacket(int id) { + Supplier> creator = c2sPacketCreators.get(id); + return creator == null ? null : creator.get(); + } + + public static int getS2CId(Packet packet) { + return s2cPacketIds.get(packet.getClass()); + } + + public static Packet createS2CPacket(int id) { + Supplier> creator = s2cPacketCreators.get(id); + return creator == null ? null : creator.get(); + } + +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java new file mode 100644 index 0000000..3f85228 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java @@ -0,0 +1,55 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.mapping.IllegalNameException; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.network.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RemoveMappingC2SPacket implements Packet { + private Entry entry; + + RemoveMappingC2SPacket() { + } + + public RemoveMappingC2SPacket(Entry entry) { + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + + if (valid) { + try { + handler.getServer().getMappings().removeByObf(entry); + } catch (IllegalNameException e) { + valid = false; + } + } + + if (!valid) { + handler.getServer().sendCorrectMapping(handler.getClient(), entry, true); + return; + } + + handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " removed the mapping for " + entry); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new RemoveMappingS2CPacket(syncId, entry)); + handler.getServer().sendMessage(Message.removeMapping(handler.getServer().getUsername(handler.getClient()), entry)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java new file mode 100644 index 0000000..70d803c --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java @@ -0,0 +1,40 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RemoveMappingS2CPacket implements Packet { + private int syncId; + private Entry entry; + + RemoveMappingS2CPacket() { + } + + public RemoveMappingS2CPacket(int syncId, Entry entry) { + this.syncId = syncId; + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(ClientPacketHandler controller) { + controller.removeMapping(new EntryReference<>(entry, entry.getName()), false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java new file mode 100644 index 0000000..e3e7e37 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java @@ -0,0 +1,64 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.mapping.IllegalNameException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.network.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RenameC2SPacket implements Packet { + private Entry entry; + private String newName; + private boolean refreshClassTree; + + RenameC2SPacket() { + } + + public RenameC2SPacket(Entry entry, String newName, boolean refreshClassTree) { + this.entry = entry; + this.newName = newName; + this.refreshClassTree = refreshClassTree; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + this.newName = PacketHelper.readString(input); + this.refreshClassTree = input.readBoolean(); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newName); + output.writeBoolean(refreshClassTree); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + + if (valid) { + try { + handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(newName)); + } catch (IllegalNameException e) { + valid = false; + } + } + + if (!valid) { + handler.getServer().sendCorrectMapping(handler.getClient(), entry, refreshClassTree); + return; + } + + handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " renamed " + entry + " to " + newName); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new RenameS2CPacket(syncId, entry, newName, refreshClassTree)); + handler.getServer().sendMessage(Message.rename(handler.getServer().getUsername(handler.getClient()), entry, newName)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java new file mode 100644 index 0000000..787e89e --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java @@ -0,0 +1,48 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RenameS2CPacket implements Packet { + private int syncId; + private Entry entry; + private String newName; + private boolean refreshClassTree; + + RenameS2CPacket() { + } + + public RenameS2CPacket(int syncId, Entry entry, String newName, boolean refreshClassTree) { + this.syncId = syncId; + this.entry = entry; + this.newName = newName; + this.refreshClassTree = refreshClassTree; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + this.newName = PacketHelper.readString(input); + this.refreshClassTree = input.readBoolean(); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newName); + output.writeBoolean(refreshClassTree); + } + + @Override + public void handle(ClientPacketHandler controller) { + controller.rename(new EntryReference<>(entry, entry.getName()), newName, refreshClassTree, false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java new file mode 100644 index 0000000..76ecbc7 --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java @@ -0,0 +1,88 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class SyncMappingsS2CPacket implements Packet { + private EntryTree mappings; + + SyncMappingsS2CPacket() { + } + + public SyncMappingsS2CPacket(EntryTree mappings) { + this.mappings = mappings; + } + + @Override + public void read(DataInput input) throws IOException { + mappings = new HashEntryTree<>(); + int size = input.readInt(); + for (int i = 0; i < size; i++) { + readEntryTreeNode(input, null); + } + } + + private void readEntryTreeNode(DataInput input, Entry parent) throws IOException { + Entry entry = PacketHelper.readEntry(input, parent, false); + EntryMapping mapping = null; + if (input.readBoolean()) { + String name = input.readUTF(); + if (input.readBoolean()) { + String javadoc = input.readUTF(); + mapping = new EntryMapping(name, javadoc); + } else { + mapping = new EntryMapping(name); + } + } + mappings.insert(entry, mapping); + int size = input.readUnsignedShort(); + for (int i = 0; i < size; i++) { + readEntryTreeNode(input, entry); + } + } + + @Override + public void write(DataOutput output) throws IOException { + List> roots = mappings.getRootNodes().collect(Collectors.toList()); + output.writeInt(roots.size()); + for (EntryTreeNode node : roots) { + writeEntryTreeNode(output, node); + } + } + + private static void writeEntryTreeNode(DataOutput output, EntryTreeNode node) throws IOException { + PacketHelper.writeEntry(output, node.getEntry(), false); + EntryMapping value = node.getValue(); + output.writeBoolean(value != null); + if (value != null) { + PacketHelper.writeString(output, value.getTargetName()); + output.writeBoolean(value.getJavadoc() != null); + if (value.getJavadoc() != null) { + PacketHelper.writeString(output, value.getJavadoc()); + } + } + Collection> children = node.getChildNodes(); + output.writeShort(children.size()); + for (EntryTreeNode child : children) { + writeEntryTreeNode(output, child); + } + } + + @Override + public void handle(ClientPacketHandler controller) { + controller.openMappings(mappings); + controller.sendPacket(new ConfirmChangeC2SPacket(EnigmaServer.DUMMY_SYNC_ID)); + } +} diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java new file mode 100644 index 0000000..b4a277a --- /dev/null +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java @@ -0,0 +1,44 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ClientPacketHandler; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class UserListS2CPacket implements Packet { + + private List users; + + UserListS2CPacket() { + } + + public UserListS2CPacket(List users) { + this.users = users; + } + + @Override + public void read(DataInput input) throws IOException { + int len = input.readUnsignedShort(); + users = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + users.add(input.readUTF()); + } + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(users.size()); + for (String user : users) { + PacketHelper.writeString(output, user); + } + } + + @Override + public void handle(ClientPacketHandler handler) { + handler.updateUserList(users); + } + +} -- cgit v1.2.3