summaryrefslogtreecommitdiff
path: root/enigma-server
diff options
context:
space:
mode:
authorGravatar Runemoro2020-06-03 13:39:42 -0400
committerGravatar GitHub2020-06-03 18:39:42 +0100
commit0f47403d0220757fed189b76e2071e25b1025cb8 (patch)
tree879bf72c4476f0a5e0d82da99d7ff2b2276bcaca /enigma-server
parentFix search dialog hanging for a short time sometimes (#250) (diff)
downloadenigma-0f47403d0220757fed189b76e2071e25b1025cb8.tar.gz
enigma-0f47403d0220757fed189b76e2071e25b1025cb8.tar.xz
enigma-0f47403d0220757fed189b76e2071e25b1025cb8.zip
Split GUI code to separate module (#242)
* Split into modules * Post merge compile fixes Co-authored-by: modmuss50 <modmuss50@gmail.com>
Diffstat (limited to 'enigma-server')
-rw-r--r--enigma-server/build.gradle6
-rw-r--r--enigma-server/docs/protocol.md366
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java29
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java200
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/EnigmaClient.java78
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/EnigmaServer.java290
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java16
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/Message.java393
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java22
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java59
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java44
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java33
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java33
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java75
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java48
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java40
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java39
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java36
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/Packet.java15
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java135
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java64
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java55
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java40
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java64
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java48
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java88
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java44
27 files changed, 2360 insertions, 0 deletions
diff --git a/enigma-server/build.gradle b/enigma-server/build.gradle
new file mode 100644
index 00000000..bf72b184
--- /dev/null
+++ b/enigma-server/build.gradle
@@ -0,0 +1,6 @@
1dependencies {
2 implementation project(':enigma')
3 implementation 'net.sf.jopt-simple:jopt-simple:6.0-alpha-3'
4}
5
6jar.manifest.attributes 'Main-Class': 'cuchaz.enigma.network.DedicatedEnigmaServer'
diff --git a/enigma-server/docs/protocol.md b/enigma-server/docs/protocol.md
new file mode 100644
index 00000000..c14ecb81
--- /dev/null
+++ b/enigma-server/docs/protocol.md
@@ -0,0 +1,366 @@
1# Enigma protocol
2Enigma uses TCP sockets for communication. Data is sent in each direction as a continuous stream, with packets being
3concatenated one after the other.
4
5In this document, data will be represented in C-like pseudocode. The primitive data types will be the same as those
6defined by Java's [DataOutputStream](https://docs.oracle.com/javase/7/docs/api/java/io/DataOutputStream.html), i.e. in
7big-endian order for multi-byte integers (`short`, `int` and `long`). The one exception is for Strings, which do *not*
8use the same modified UTF format as in `DataOutputStream`, I repeat, the normal `writeUTF` method in `DataOutputStream`
9(and the corresponding method in `DataInputStream`) should *not* be used. Instead, there is a custom `utf` struct for
10Strings, see below.
11
12## Login protocol
13```
14Client Server
15| |
16| Login |
17| >>>>>>>>>>>>> |
18| |
19| SyncMappings |
20| <<<<<<<<<<<<< |
21| |
22| ConfirmChange |
23| >>>>>>>>>>>>> |
24```
251. On connect, the client sends a login packet to the server. This allows the server to test the validity of the client,
26 as well as allowing the client to declare metadata about itself, such as the username.
271. After validating the login packet, the server sends all its mappings to the client, and the client will apply them.
281. Upon receiving the mappings, the client sends a `ConfirmChange` packet with `sync_id` set to 0, to confirm that it
29 has received the mappings and is in sync with the server. Once the server receives this packet, the client will be
30 allowed to modify mappings.
31
32The server will not accept any other packets from the client until this entire exchange has been completed.
33
34## Kicking clients
35When the server kicks a client, it may optionally send a `Kick` packet immediately before closing the connection, which
36contains the reason why the client was kicked (so the client can display it to the user). This is not required though -
37the server may simply terminate the connection.
38
39## Changing mappings
40This section uses the example of renaming, but the same pattern applies to all mapping changes.
41```
42Client A Server Client B
43| | |
44| RenameC2S | |
45| >>>>>>>>> | |
46| | |
47| | RenameS2C |
48| | >>>>>>>>>>>>> |
49| | |
50| | ConfirmChange |
51| | <<<<<<<<<<<<< |
52```
53
541. Client A validates the name and updates the mapping client-side to give the impression there is no latency >:)
551. Client A sends a rename packet to the server, notifying it of the rename.
561. The server assesses the validity of the rename. If it is invalid for whatever reason (e.g. the mapping was locked or
57 the name contains invalid characters), then the server sends an appropriate packet back to client A to revert the
58 change, with `sync_id` set to 0. The server will ignore any `ConfirmChange` packets it receives in response to this.
591. If the rename was valid, the server will lock all clients except client A from being able to modify this mapping, and
60 then send an appropriate packet to all clients except client A notifying them of this rename. The `sync_id` will be a
61 unique non-zero value identifying this change.
621. Each client responds to this packet by updating their mappings locally to reflect this change, then sending a
63 `ConfirmChange` packet with the same `sync_id` as the one in the packet they received, to confirm that they have
64 received the change.
651. When the server receives the `ConfirmChange` packet, and another change to that mapping hasn't occurred since, the
66 server will unlock that mapping for that client and allow them to make changes again.
67
68## Packets
69```c
70struct Packet {
71 unsigned short packet_id;
72 data[]; // depends on packet_id
73}
74```
75The IDs for client-to-server packets are as follows:
76- 0: `Login`
77- 1: `ConfirmChange`
78- 2: `Rename`
79- 3: `RemoveMapping`
80- 4: `ChangeDocs`
81- 5: `MarkDeobfuscated`
82- 6: `Message`
83
84The IDs for server-to-client packets are as follows:
85- 0: `Kick`
86- 1: `SyncMappings`
87- 2: `Rename`
88- 3: `RemoveMapping`
89- 4: `ChangeDocs`
90- 5: `MarkDeobfuscated`
91- 6: `Message`
92- 7: `UserList`
93
94### The utf struct
95```c
96struct utf {
97 unsigned short length;
98 byte data[length];
99}
100```
101- `length`: The number of bytes in the UTF-8 encoding of the string. Note, this may not be the same as the number of
102 Unicode characters in the string.
103- `data`: A standard UTF-8 encoded byte array representing the string.
104
105### The Entry struct
106```c
107enum EntryType {
108 ENTRY_CLASS = 0, ENTRY_FIELD = 1, ENTRY_METHOD = 2, ENTRY_LOCAL_VAR = 3;
109}
110struct Entry {
111 unsigned byte type;
112 boolean has_parent;
113 if<has_parent> {
114 Entry parent;
115 }
116 utf name;
117 boolean has_javadoc;
118 if<has_javadoc> {
119 utf javadoc;
120 }
121 if<type == ENTRY_FIELD || type == ENTRY_METHOD> {
122 utf descriptor;
123 }
124 if<type == ENTRY_LOCAL_VAR> {
125 unsigned short index;
126 boolean parameter;
127 }
128}
129```
130- `type`: The type of entry this is. One of `ENTRY_CLASS`, `ENTRY_FIELD`, `ENTRY_METHOD` or `ENTRY_LOCAL_VAR`.
131- `parent`: The parent entry. Only class entries may have no parent. fields, methods and inner classes must have their
132 containing class as their parent. Local variables have a method as a parent.
133- `name`: The class/field/method/variable name.
134- `javadoc`: The javadoc of an entry, if present.
135- `descriptor`: The field/method descriptor.
136- `index`: The index of the local variable in the local variable table.
137- `parameter`: Whether the local variable is a parameter.
138
139### The Message struct
140```c
141enum MessageType {
142 MESSAGE_CHAT = 0,
143 MESSAGE_CONNECT = 1,
144 MESSAGE_DISCONNECT = 2,
145 MESSAGE_EDIT_DOCS = 3,
146 MESSAGE_MARK_DEOBF = 4,
147 MESSAGE_REMOVE_MAPPING = 5,
148 MESSAGE_RENAME = 6
149};
150typedef unsigned byte message_type_t;
151
152struct Message {
153 message_type_t type;
154 union { // Note that the size of this varies depending on type, it is not constant size
155 struct {
156 utf user;
157 utf message;
158 } chat;
159 struct {
160 utf user;
161 } connect;
162 struct {
163 utf user;
164 } disconnect;
165 struct {
166 utf user;
167 Entry entry;
168 } edit_docs;
169 struct {
170 utf user;
171 Entry entry;
172 } mark_deobf;
173 struct {
174 utf user;
175 Entry entry;
176 } remove_mapping;
177 struct {
178 utf user;
179 Entry entry;
180 utf new_name;
181 } rename;
182 } data;
183};
184```
185- `type`: The type of message this is. One of `MESSAGE_CHAT`, `MESSAGE_CONNECT`, `MESSAGE_DISCONNECT`,
186 `MESSAGE_EDIT_DOCS`, `MESSAGE_MARK_DEOBF`, `MESSAGE_REMOVE_MAPPING`, `MESSAGE_RENAME`.
187- `chat`: Chat message. Use in case `type` is `MESSAGE_CHAT`
188- `connect`: Sent when a user connects. Use in case `type` is `MESSAGE_CONNECT`
189- `disconnect`: Sent when a user disconnects. Use in case `type` is `MESSAGE_DISCONNECT`
190- `edit_docs`: Sent when a user edits the documentation of an entry. Use in case `type` is `MESSAGE_EDIT_DOCS`
191- `mark_deobf`: Sent when a user marks an entry as deobfuscated. Use in case `type` is `MESSAGE_MARK_DEOBF`
192- `remove_mapping`: Sent when a user removes a mapping. Use in case `type` is `MESSAGE_REMOVE_MAPPING`
193- `rename`: Sent when a user renames an entry. Use in case `type` is `MESSAGE_RENAME`
194- `user`: The user that performed the action.
195- `message`: The message the user sent.
196- `entry`: The entry that was modified.
197- `new_name`: The new name for the entry.
198
199
200### Login (client-to-server)
201```c
202struct LoginC2SPacket {
203 unsigned short protocol_version;
204 byte checksum[20];
205 unsigned byte password_length;
206 char password[password_length];
207 utf username;
208}
209```
210- `protocol_version`: the version of the protocol. If the version does not match on the server, then the client will be
211 kicked immediately. Currently always equal to 0.
212- `checksum`: the SHA-1 hash of the JAR file the client has open. If this does not match the SHA-1 hash of the JAR file
213 the server has open, the client will be kicked.
214- `password`: the password needed to log into the server. Note that each `char` is 2 bytes, as per the Java data type.
215 If this password is incorrect, the client will be kicked.
216- `username`: the username of the user logging in. If the username is not unique, the client will be kicked.
217
218### ConfirmChange (client-to-server)
219```c
220struct ConfirmChangeC2SPacket {
221 unsigned short sync_id;
222}
223```
224- `sync_id`: the sync ID to confirm.
225
226### Rename (client-to-server)
227```c
228struct RenameC2SPacket {
229 Entry obf_entry;
230 utf new_name;
231 boolean refresh_class_tree;
232}
233```
234- `obf_entry`: the obfuscated name and descriptor of the entry to rename.
235- `new_name`: what to rename the entry to.
236- `refresh_class_tree`: whether the class tree on the sidebar of Enigma needs refreshing as a result of this change.
237
238### RemoveMapping (client-to-server)
239```c
240struct RemoveMappingC2SPacket {
241 Entry obf_entry;
242}
243```
244- `obf_entry`: the obfuscated name and descriptor of the entry to remove the mapping for.
245
246### ChangeDocs (client-to-server)
247```c
248struct ChangeDocsC2SPacket {
249 Entry obf_entry;
250 utf new_docs;
251}
252```
253- `obf_entry`: the obfuscated name and descriptor of the entry to change the documentation for.
254- `new_docs`: the new documentation for this entry, or an empty string to remove the documentation.
255
256### MarkDeobfuscated (client-to-server)
257```c
258struct MarkDeobfuscatedC2SPacket {
259 Entry obf_entry;
260}
261```
262- `obf_entry`: the obfuscated name and descriptor of the entry to mark as deobfuscated.
263
264### Message (client-to-server)
265```c
266struct MessageC2SPacket {
267 utf message;
268}
269```
270- `message`: The text message the user sent.
271
272### Kick (server-to-client)
273```c
274struct KickS2CPacket {
275 utf reason;
276}
277```
278- `reason`: the reason for the kick, may or may not be a translation key for the client to display to the user.
279
280### SyncMappings (server-to-client)
281```c
282struct SyncMappingsS2CPacket {
283 int num_roots;
284 MappingNode roots[num_roots];
285}
286struct MappingNode {
287 NoParentEntry obf_entry;
288 boolean is_named;
289 if<is_named> {
290 utf name;
291 boolean has_javadoc;
292 if<has_javadoc> {
293 utf javadoc;
294 }
295 }
296 unsigned short children_count;
297 MappingNode children[children_count];
298}
299typedef { Entry but without the has_parent or parent fields } NoParentEntry;
300```
301- `roots`: The root mapping nodes, containing all the entries without parents.
302- `obf_entry`: The value of a node, containing the obfuscated name and descriptor of the entry.
303- `name`: The deobfuscated name of the entry, if it has a mapping.
304- `javadoc`: The documentation for the entry, if it is named and has documentation.
305- `children`: The children of this node
306
307### Rename (server-to-client)
308```c
309struct RenameS2CPacket {
310 unsigned short sync_id;
311 Entry obf_entry;
312 utf new_name;
313 boolean refresh_class_tree;
314}
315```
316- `sync_id`: the sync ID of the change for locking purposes.
317- `obf_entry`: the obfuscated name and descriptor of the entry to rename.
318- `new_name`: what to rename the entry to.
319- `refresh_class_tree`: whether the class tree on the sidebar of Enigma needs refreshing as a result of this change.
320
321### RemoveMapping (server-to-client)
322```c
323struct RemoveMappingS2CPacket {
324 unsigned short sync_id;
325 Entry obf_entry;
326}
327```
328- `sync_id`: the sync ID of the change for locking purposes.
329- `obf_entry`: the obfuscated name and descriptor of the entry to remove the mapping for.
330
331### ChangeDocs (server-to-client)
332```c
333struct ChangeDocsS2CPacket {
334 unsigned short sync_id;
335 Entry obf_entry;
336 utf new_docs;
337}
338```
339- `sync_id`: the sync ID of the change for locking purposes.
340- `obf_entry`: the obfuscated name and descriptor of the entry to change the documentation for.
341- `new_docs`: the new documentation for this entry, or an empty string to remove the documentation.
342
343### MarkDeobfuscated (server-to-client)
344```c
345struct MarkDeobfuscatedS2CPacket {
346 unsigned short sync_id;
347 Entry obf_entry;
348}
349```
350- `sync_id`: the sync ID of the change for locking purposes.
351- `obf_entry`: the obfuscated name and descriptor of the entry to mark as deobfuscated.
352
353### Message (server-to-client)
354```c
355struct MessageS2CPacket {
356 Message message;
357}
358```
359
360### UserList (server-to-client)
361```c
362struct UserListS2CPacket {
363 unsigned short len;
364 utf user[len];
365}
366```
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 00000000..720744bf
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java
@@ -0,0 +1,29 @@
1package cuchaz.enigma.network;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.translation.mapping.EntryMapping;
5import cuchaz.enigma.translation.mapping.tree.EntryTree;
6import cuchaz.enigma.network.packet.Packet;
7import cuchaz.enigma.translation.representation.entry.Entry;
8
9import java.util.List;
10
11public interface ClientPacketHandler {
12 void openMappings(EntryTree<EntryMapping> mappings);
13
14 void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree, boolean jumpToReference);
15
16 void removeMapping(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference);
17
18 void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs, boolean jumpToReference);
19
20 void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference);
21
22 void disconnectIfConnected(String reason);
23
24 void sendPacket(Packet<ServerPacketHandler> packet);
25
26 void addMessage(Message message);
27
28 void updateUserList(List<String> users);
29}
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 00000000..924302f3
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java
@@ -0,0 +1,200 @@
1package cuchaz.enigma.network;
2
3import com.google.common.io.MoreFiles;
4import cuchaz.enigma.*;
5import cuchaz.enigma.translation.mapping.serde.MappingParseException;
6import cuchaz.enigma.translation.mapping.EntryRemapper;
7import cuchaz.enigma.translation.mapping.serde.MappingFormat;
8import cuchaz.enigma.utils.Utils;
9import joptsimple.OptionParser;
10import joptsimple.OptionSet;
11import joptsimple.OptionSpec;
12import joptsimple.ValueConverter;
13
14import java.io.IOException;
15import java.io.PrintWriter;
16import java.nio.file.Files;
17import java.nio.file.Path;
18import java.nio.file.Paths;
19import java.util.concurrent.BlockingQueue;
20import java.util.concurrent.Executors;
21import java.util.concurrent.LinkedBlockingDeque;
22import java.util.concurrent.TimeUnit;
23
24public class DedicatedEnigmaServer extends EnigmaServer {
25
26 private final EnigmaProfile profile;
27 private final MappingFormat mappingFormat;
28 private final Path mappingsFile;
29 private final PrintWriter log;
30 private BlockingQueue<Runnable> tasks = new LinkedBlockingDeque<>();
31
32 public DedicatedEnigmaServer(
33 byte[] jarChecksum,
34 char[] password,
35 EnigmaProfile profile,
36 MappingFormat mappingFormat,
37 Path mappingsFile,
38 PrintWriter log,
39 EntryRemapper mappings,
40 int port
41 ) {
42 super(jarChecksum, password, mappings, port);
43 this.profile = profile;
44 this.mappingFormat = mappingFormat;
45 this.mappingsFile = mappingsFile;
46 this.log = log;
47 }
48
49 @Override
50 protected void runOnThread(Runnable task) {
51 tasks.add(task);
52 }
53
54 @Override
55 public void log(String message) {
56 super.log(message);
57 log.println(message);
58 }
59
60 public static void main(String[] args) {
61 OptionParser parser = new OptionParser();
62
63 OptionSpec<Path> jarOpt = parser.accepts("jar", "Jar file to open at startup")
64 .withRequiredArg()
65 .required()
66 .withValuesConvertedBy(PathConverter.INSTANCE);
67
68 OptionSpec<Path> mappingsOpt = parser.accepts("mappings", "Mappings file to open at startup")
69 .withRequiredArg()
70 .required()
71 .withValuesConvertedBy(PathConverter.INSTANCE);
72
73 OptionSpec<Path> profileOpt = parser.accepts("profile", "Profile json to apply at startup")
74 .withRequiredArg()
75 .withValuesConvertedBy(PathConverter.INSTANCE);
76
77 OptionSpec<Integer> portOpt = parser.accepts("port", "Port to run the server on")
78 .withOptionalArg()
79 .ofType(Integer.class)
80 .defaultsTo(EnigmaServer.DEFAULT_PORT);
81
82 OptionSpec<String> passwordOpt = parser.accepts("password", "The password to join the server")
83 .withRequiredArg()
84 .defaultsTo("");
85
86 OptionSpec<Path> logFileOpt = parser.accepts("log", "The log file to write to")
87 .withRequiredArg()
88 .withValuesConvertedBy(PathConverter.INSTANCE)
89 .defaultsTo(Paths.get("log.txt"));
90
91 OptionSet parsedArgs = parser.parse(args);
92 Path jar = parsedArgs.valueOf(jarOpt);
93 Path mappingsFile = parsedArgs.valueOf(mappingsOpt);
94 Path profileFile = parsedArgs.valueOf(profileOpt);
95 int port = parsedArgs.valueOf(portOpt);
96 char[] password = parsedArgs.valueOf(passwordOpt).toCharArray();
97 if (password.length > EnigmaServer.MAX_PASSWORD_LENGTH) {
98 System.err.println("Password too long, must be at most " + EnigmaServer.MAX_PASSWORD_LENGTH + " characters");
99 System.exit(1);
100 }
101 Path logFile = parsedArgs.valueOf(logFileOpt);
102
103 System.out.println("Starting Enigma server");
104 DedicatedEnigmaServer server;
105 try {
106 byte[] checksum = Utils.zipSha1(parsedArgs.valueOf(jarOpt));
107
108 EnigmaProfile profile = EnigmaProfile.read(profileFile);
109 Enigma enigma = Enigma.builder().setProfile(profile).build();
110 System.out.println("Indexing Jar...");
111 EnigmaProject project = enigma.openJar(jar, ProgressListener.none());
112
113 MappingFormat mappingFormat = MappingFormat.ENIGMA_DIRECTORY;
114 EntryRemapper mappings;
115 if (!Files.exists(mappingsFile)) {
116 mappings = EntryRemapper.empty(project.getJarIndex());
117 } else {
118 System.out.println("Reading mappings...");
119 if (Files.isDirectory(mappingsFile)) {
120 mappingFormat = MappingFormat.ENIGMA_DIRECTORY;
121 } else if ("zip".equalsIgnoreCase(MoreFiles.getFileExtension(mappingsFile))) {
122 mappingFormat = MappingFormat.ENIGMA_ZIP;
123 } else {
124 mappingFormat = MappingFormat.ENIGMA_FILE;
125 }
126 mappings = EntryRemapper.mapped(project.getJarIndex(), mappingFormat.read(mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters()));
127 }
128
129 PrintWriter log = new PrintWriter(Files.newBufferedWriter(logFile));
130
131 server = new DedicatedEnigmaServer(checksum, password, profile, mappingFormat, mappingsFile, log, mappings, port);
132 server.start();
133 System.out.println("Server started");
134 } catch (IOException | MappingParseException e) {
135 System.err.println("Error starting server!");
136 e.printStackTrace();
137 System.exit(1);
138 return;
139 }
140
141 // noinspection RedundantSuppression
142 // noinspection Convert2MethodRef - javac 8 bug
143 Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> server.runOnThread(() -> server.saveMappings()), 0, 1, TimeUnit.MINUTES);
144 Runtime.getRuntime().addShutdownHook(new Thread(server::saveMappings));
145
146 while (true) {
147 try {
148 server.tasks.take().run();
149 } catch (InterruptedException e) {
150 break;
151 }
152 }
153 }
154
155 @Override
156 public synchronized void stop() {
157 super.stop();
158 System.exit(0);
159 }
160
161 private void saveMappings() {
162 mappingFormat.write(getMappings().getObfToDeobf(), getMappings().takeMappingDelta(), mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters());
163 log.flush();
164 }
165
166 public static class PathConverter implements ValueConverter<Path> {
167 public static final ValueConverter<Path> INSTANCE = new PathConverter();
168
169 PathConverter() {
170 }
171
172 @Override
173 public Path convert(String path) {
174 // expand ~ to the home dir
175 if (path.startsWith("~")) {
176 // get the home dir
177 Path dirHome = Paths.get(System.getProperty("user.home"));
178
179 // is the path just ~/ or is it ~user/ ?
180 if (path.startsWith("~/")) {
181 return dirHome.resolve(path.substring(2));
182 } else {
183 return dirHome.getParent().resolve(path.substring(1));
184 }
185 }
186
187 return Paths.get(path);
188 }
189
190 @Override
191 public Class<? extends Path> valueType() {
192 return Path.class;
193 }
194
195 @Override
196 public String valuePattern() {
197 return "path";
198 }
199 }
200}
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 00000000..71bd011c
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaClient.java
@@ -0,0 +1,78 @@
1package cuchaz.enigma.network;
2
3import cuchaz.enigma.network.packet.Packet;
4import cuchaz.enigma.network.packet.PacketRegistry;
5
6import javax.swing.*;
7import java.io.*;
8import java.net.Socket;
9import java.net.SocketException;
10
11public class EnigmaClient {
12
13 private final ClientPacketHandler controller;
14
15 private final String ip;
16 private final int port;
17 private Socket socket;
18 private DataOutput output;
19
20 public EnigmaClient(ClientPacketHandler controller, String ip, int port) {
21 this.controller = controller;
22 this.ip = ip;
23 this.port = port;
24 }
25
26 public void connect() throws IOException {
27 socket = new Socket(ip, port);
28 output = new DataOutputStream(socket.getOutputStream());
29 Thread thread = new Thread(() -> {
30 try {
31 DataInput input = new DataInputStream(socket.getInputStream());
32 while (true) {
33 int packetId;
34 try {
35 packetId = input.readUnsignedByte();
36 } catch (EOFException | SocketException e) {
37 break;
38 }
39 Packet<ClientPacketHandler> packet = PacketRegistry.createS2CPacket(packetId);
40 if (packet == null) {
41 throw new IOException("Received invalid packet id " + packetId);
42 }
43 packet.read(input);
44 SwingUtilities.invokeLater(() -> packet.handle(controller));
45 }
46 } catch (IOException e) {
47 controller.disconnectIfConnected(e.toString());
48 return;
49 }
50 controller.disconnectIfConnected("Disconnected");
51 });
52 thread.setName("Client I/O thread");
53 thread.setDaemon(true);
54 thread.start();
55 }
56
57 public synchronized void disconnect() {
58 if (socket != null && !socket.isClosed()) {
59 try {
60 socket.close();
61 } catch (IOException e1) {
62 System.err.println("Failed to close socket");
63 e1.printStackTrace();
64 }
65 }
66 }
67
68
69 public void sendPacket(Packet<ServerPacketHandler> packet) {
70 try {
71 output.writeByte(PacketRegistry.getC2SId(packet));
72 packet.write(output);
73 } catch (IOException e) {
74 controller.disconnectIfConnected(e.toString());
75 }
76 }
77
78}
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 00000000..6027a6bd
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/EnigmaServer.java
@@ -0,0 +1,290 @@
1package cuchaz.enigma.network;
2
3import cuchaz.enigma.network.packet.KickS2CPacket;
4import cuchaz.enigma.network.packet.MessageS2CPacket;
5import cuchaz.enigma.network.packet.Packet;
6import cuchaz.enigma.network.packet.PacketRegistry;
7import cuchaz.enigma.network.packet.RemoveMappingS2CPacket;
8import cuchaz.enigma.network.packet.RenameS2CPacket;
9import cuchaz.enigma.network.packet.UserListS2CPacket;
10import cuchaz.enigma.translation.mapping.EntryMapping;
11import cuchaz.enigma.translation.mapping.EntryRemapper;
12import cuchaz.enigma.translation.representation.entry.Entry;
13
14import java.io.DataInput;
15import java.io.DataInputStream;
16import java.io.DataOutput;
17import java.io.DataOutputStream;
18import java.io.EOFException;
19import java.io.IOException;
20import java.net.ServerSocket;
21import java.net.Socket;
22import java.net.SocketException;
23import java.util.ArrayList;
24import java.util.Collections;
25import java.util.HashMap;
26import java.util.HashSet;
27import java.util.List;
28import java.util.Map;
29import java.util.Set;
30import java.util.concurrent.CopyOnWriteArrayList;
31
32public abstract class EnigmaServer {
33
34 // https://discordapp.com/channels/507304429255393322/566418023372816394/700292322918793347
35 public static final int DEFAULT_PORT = 34712;
36 public static final int PROTOCOL_VERSION = 0;
37 public static final String OWNER_USERNAME = "Owner";
38 public static final int CHECKSUM_SIZE = 20;
39 public static final int MAX_PASSWORD_LENGTH = 255; // length is written as a byte in the login packet
40
41 private final int port;
42 private ServerSocket socket;
43 private List<Socket> clients = new CopyOnWriteArrayList<>();
44 private Map<Socket, String> usernames = new HashMap<>();
45 private Set<Socket> unapprovedClients = new HashSet<>();
46
47 private final byte[] jarChecksum;
48 private final char[] password;
49
50 public static final int DUMMY_SYNC_ID = 0;
51 private final EntryRemapper mappings;
52 private Map<Entry<?>, Integer> syncIds = new HashMap<>();
53 private Map<Integer, Entry<?>> inverseSyncIds = new HashMap<>();
54 private Map<Integer, Set<Socket>> clientsNeedingConfirmation = new HashMap<>();
55 private int nextSyncId = DUMMY_SYNC_ID + 1;
56
57 private static int nextIoId = 0;
58
59 public EnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) {
60 this.jarChecksum = jarChecksum;
61 this.password = password;
62 this.mappings = mappings;
63 this.port = port;
64 }
65
66 public void start() throws IOException {
67 socket = new ServerSocket(port);
68 log("Server started on " + socket.getInetAddress() + ":" + port);
69 Thread thread = new Thread(() -> {
70 try {
71 while (!socket.isClosed()) {
72 acceptClient();
73 }
74 } catch (SocketException e) {
75 System.out.println("Server closed");
76 } catch (IOException e) {
77 e.printStackTrace();
78 }
79 });
80 thread.setName("Server client listener");
81 thread.setDaemon(true);
82 thread.start();
83 }
84
85 private void acceptClient() throws IOException {
86 Socket client = socket.accept();
87 clients.add(client);
88 Thread thread = new Thread(() -> {
89 try {
90 DataInput input = new DataInputStream(client.getInputStream());
91 while (true) {
92 int packetId;
93 try {
94 packetId = input.readUnsignedByte();
95 } catch (EOFException | SocketException e) {
96 break;
97 }
98 Packet<ServerPacketHandler> packet = PacketRegistry.createC2SPacket(packetId);
99 if (packet == null) {
100 throw new IOException("Received invalid packet id " + packetId);
101 }
102 packet.read(input);
103 runOnThread(() -> packet.handle(new ServerPacketHandler(client, this)));
104 }
105 } catch (IOException e) {
106 kick(client, e.toString());
107 e.printStackTrace();
108 return;
109 }
110 kick(client, "disconnect.disconnected");
111 });
112 thread.setName("Server I/O thread #" + (nextIoId++));
113 thread.setDaemon(true);
114 thread.start();
115 }
116
117 public void stop() {
118 runOnThread(() -> {
119 if (socket != null && !socket.isClosed()) {
120 for (Socket client : clients) {
121 kick(client, "disconnect.server_closed");
122 }
123 try {
124 socket.close();
125 } catch (IOException e) {
126 System.err.println("Failed to close server socket");
127 e.printStackTrace();
128 }
129 }
130 });
131 }
132
133 public void kick(Socket client, String reason) {
134 if (!clients.remove(client)) return;
135
136 sendPacket(client, new KickS2CPacket(reason));
137
138 clientsNeedingConfirmation.values().removeIf(list -> {
139 list.remove(client);
140 return list.isEmpty();
141 });
142 String username = usernames.remove(client);
143 try {
144 client.close();
145 } catch (IOException e) {
146 System.err.println("Failed to close server client socket");
147 e.printStackTrace();
148 }
149
150 if (username != null) {
151 System.out.println("Kicked " + username + " because " + reason);
152 sendMessage(Message.disconnect(username));
153 }
154 sendUsernamePacket();
155 }
156
157 public boolean isUsernameTaken(String username) {
158 return usernames.containsValue(username);
159 }
160
161 public void setUsername(Socket client, String username) {
162 usernames.put(client, username);
163 sendUsernamePacket();
164 }
165
166 private void sendUsernamePacket() {
167 List<String> usernames = new ArrayList<>(this.usernames.values());
168 Collections.sort(usernames);
169 sendToAll(new UserListS2CPacket(usernames));
170 }
171
172 public String getUsername(Socket client) {
173 return usernames.get(client);
174 }
175
176 public void sendPacket(Socket client, Packet<ClientPacketHandler> packet) {
177 if (!client.isClosed()) {
178 int packetId = PacketRegistry.getS2CId(packet);
179 try {
180 DataOutput output = new DataOutputStream(client.getOutputStream());
181 output.writeByte(packetId);
182 packet.write(output);
183 } catch (IOException e) {
184 if (!(packet instanceof KickS2CPacket)) {
185 kick(client, e.toString());
186 e.printStackTrace();
187 }
188 }
189 }
190 }
191
192 public void sendToAll(Packet<ClientPacketHandler> packet) {
193 for (Socket client : clients) {
194 sendPacket(client, packet);
195 }
196 }
197
198 public void sendToAllExcept(Socket excluded, Packet<ClientPacketHandler> packet) {
199 for (Socket client : clients) {
200 if (client != excluded) {
201 sendPacket(client, packet);
202 }
203 }
204 }
205
206 public boolean canModifyEntry(Socket client, Entry<?> entry) {
207 if (unapprovedClients.contains(client)) {
208 return false;
209 }
210
211 Integer syncId = syncIds.get(entry);
212 if (syncId == null) {
213 return true;
214 }
215 Set<Socket> clients = clientsNeedingConfirmation.get(syncId);
216 return clients == null || !clients.contains(client);
217 }
218
219 public int lockEntry(Socket exception, Entry<?> entry) {
220 int syncId = nextSyncId;
221 nextSyncId++;
222 // sync id is sent as an unsigned short, can't have more than 65536
223 if (nextSyncId == 65536) {
224 nextSyncId = DUMMY_SYNC_ID + 1;
225 }
226 Integer oldSyncId = syncIds.get(entry);
227 if (oldSyncId != null) {
228 clientsNeedingConfirmation.remove(oldSyncId);
229 }
230 syncIds.put(entry, syncId);
231 inverseSyncIds.put(syncId, entry);
232 Set<Socket> clients = new HashSet<>(this.clients);
233 clients.remove(exception);
234 clientsNeedingConfirmation.put(syncId, clients);
235 return syncId;
236 }
237
238 public void confirmChange(Socket client, int syncId) {
239 if (usernames.containsKey(client)) {
240 unapprovedClients.remove(client);
241 }
242
243 Set<Socket> clients = clientsNeedingConfirmation.get(syncId);
244 if (clients != null) {
245 clients.remove(client);
246 if (clients.isEmpty()) {
247 clientsNeedingConfirmation.remove(syncId);
248 syncIds.remove(inverseSyncIds.remove(syncId));
249 }
250 }
251 }
252
253 public void sendCorrectMapping(Socket client, Entry<?> entry, boolean refreshClassTree) {
254 EntryMapping oldMapping = mappings.getDeobfMapping(entry);
255 String oldName = oldMapping == null ? null : oldMapping.getTargetName();
256 if (oldName == null) {
257 sendPacket(client, new RemoveMappingS2CPacket(DUMMY_SYNC_ID, entry));
258 } else {
259 sendPacket(client, new RenameS2CPacket(0, entry, oldName, refreshClassTree));
260 }
261 }
262
263 protected abstract void runOnThread(Runnable task);
264
265 public void log(String message) {
266 System.out.println(message);
267 }
268
269 protected boolean isRunning() {
270 return !socket.isClosed();
271 }
272
273 public byte[] getJarChecksum() {
274 return jarChecksum;
275 }
276
277 public char[] getPassword() {
278 return password;
279 }
280
281 public EntryRemapper getMappings() {
282 return mappings;
283 }
284
285 public void sendMessage(Message message) {
286 log(String.format("[MSG] %s", message.translate()));
287 sendToAll(new MessageS2CPacket(message));
288 }
289
290}
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 00000000..21c6825b
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java
@@ -0,0 +1,16 @@
1package cuchaz.enigma.network;
2
3import cuchaz.enigma.translation.mapping.EntryRemapper;
4
5import javax.swing.*;
6
7public class IntegratedEnigmaServer extends EnigmaServer {
8 public IntegratedEnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) {
9 super(jarChecksum, password, mappings, port);
10 }
11
12 @Override
13 protected void runOnThread(Runnable task) {
14 SwingUtilities.invokeLater(task);
15 }
16}
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 00000000..c1578387
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/Message.java
@@ -0,0 +1,393 @@
1package cuchaz.enigma.network;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6import java.util.Objects;
7
8import cuchaz.enigma.network.packet.PacketHelper;
9import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.utils.I18n;
11
12public abstract class Message {
13
14 public final String user;
15
16 public static Chat chat(String user, String message) {
17 return new Chat(user, message);
18 }
19
20 public static Connect connect(String user) {
21 return new Connect(user);
22 }
23
24 public static Disconnect disconnect(String user) {
25 return new Disconnect(user);
26 }
27
28 public static EditDocs editDocs(String user, Entry<?> entry) {
29 return new EditDocs(user, entry);
30 }
31
32 public static MarkDeobf markDeobf(String user, Entry<?> entry) {
33 return new MarkDeobf(user, entry);
34 }
35
36 public static RemoveMapping removeMapping(String user, Entry<?> entry) {
37 return new RemoveMapping(user, entry);
38 }
39
40 public static Rename rename(String user, Entry<?> entry, String newName) {
41 return new Rename(user, entry, newName);
42 }
43
44 public abstract String translate();
45
46 public abstract Type getType();
47
48 public static Message read(DataInput input) throws IOException {
49 byte typeId = input.readByte();
50 if (typeId < 0 || typeId >= Type.values().length) {
51 throw new IOException(String.format("Invalid message type ID %d", typeId));
52 }
53 Type type = Type.values()[typeId];
54 String user = input.readUTF();
55 switch (type) {
56 case CHAT:
57 String message = input.readUTF();
58 return chat(user, message);
59 case CONNECT:
60 return connect(user);
61 case DISCONNECT:
62 return disconnect(user);
63 case EDIT_DOCS:
64 Entry<?> entry = PacketHelper.readEntry(input);
65 return editDocs(user, entry);
66 case MARK_DEOBF:
67 entry = PacketHelper.readEntry(input);
68 return markDeobf(user, entry);
69 case REMOVE_MAPPING:
70 entry = PacketHelper.readEntry(input);
71 return removeMapping(user, entry);
72 case RENAME:
73 entry = PacketHelper.readEntry(input);
74 String newName = input.readUTF();
75 return rename(user, entry, newName);
76 default:
77 throw new IllegalStateException("unreachable");
78 }
79 }
80
81 public void write(DataOutput output) throws IOException {
82 output.writeByte(getType().ordinal());
83 PacketHelper.writeString(output, user);
84 }
85
86 private Message(String user) {
87 this.user = user;
88 }
89
90 @Override
91 public boolean equals(Object o) {
92 if (this == o) return true;
93 if (o == null || getClass() != o.getClass()) return false;
94 Message message = (Message) o;
95 return Objects.equals(user, message.user);
96 }
97
98 @Override
99 public int hashCode() {
100 return Objects.hash(user);
101 }
102
103 public enum Type {
104 CHAT,
105 CONNECT,
106 DISCONNECT,
107 EDIT_DOCS,
108 MARK_DEOBF,
109 REMOVE_MAPPING,
110 RENAME,
111 }
112
113 public static final class Chat extends Message {
114
115 public final String message;
116
117 private Chat(String user, String message) {
118 super(user);
119 this.message = message;
120 }
121
122 @Override
123 public void write(DataOutput output) throws IOException {
124 super.write(output);
125 PacketHelper.writeString(output, message);
126 }
127
128 @Override
129 public String translate() {
130 return String.format(I18n.translate("message.chat.text"), user, message);
131 }
132
133 @Override
134 public Type getType() {
135 return Type.CHAT;
136 }
137
138 @Override
139 public boolean equals(Object o) {
140 if (this == o) return true;
141 if (o == null || getClass() != o.getClass()) return false;
142 if (!super.equals(o)) return false;
143 Chat chat = (Chat) o;
144 return Objects.equals(message, chat.message);
145 }
146
147 @Override
148 public int hashCode() {
149 return Objects.hash(super.hashCode(), message);
150 }
151
152 @Override
153 public String toString() {
154 return String.format("Message.Chat { user: '%s', message: '%s' }", user, message);
155 }
156
157 }
158
159 public static final class Connect extends Message {
160
161 private Connect(String user) {
162 super(user);
163 }
164
165 @Override
166 public String translate() {
167 return String.format(I18n.translate("message.connect.text"), user);
168 }
169
170 @Override
171 public Type getType() {
172 return Type.CONNECT;
173 }
174
175 @Override
176 public String toString() {
177 return String.format("Message.Connect { user: '%s' }", user);
178 }
179
180 }
181
182 public static final class Disconnect extends Message {
183
184 private Disconnect(String user) {
185 super(user);
186 }
187
188 @Override
189 public String translate() {
190 return String.format(I18n.translate("message.disconnect.text"), user);
191 }
192
193 @Override
194 public Type getType() {
195 return Type.DISCONNECT;
196 }
197
198 @Override
199 public String toString() {
200 return String.format("Message.Disconnect { user: '%s' }", user);
201 }
202
203 }
204
205 public static final class EditDocs extends Message {
206
207 public final Entry<?> entry;
208
209 private EditDocs(String user, Entry<?> entry) {
210 super(user);
211 this.entry = entry;
212 }
213
214 @Override
215 public void write(DataOutput output) throws IOException {
216 super.write(output);
217 PacketHelper.writeEntry(output, entry);
218 }
219
220 @Override
221 public String translate() {
222 return String.format(I18n.translate("message.edit_docs.text"), user, entry);
223 }
224
225 @Override
226 public Type getType() {
227 return Type.EDIT_DOCS;
228 }
229
230 @Override
231 public boolean equals(Object o) {
232 if (this == o) return true;
233 if (o == null || getClass() != o.getClass()) return false;
234 if (!super.equals(o)) return false;
235 EditDocs editDocs = (EditDocs) o;
236 return Objects.equals(entry, editDocs.entry);
237 }
238
239 @Override
240 public int hashCode() {
241 return Objects.hash(super.hashCode(), entry);
242 }
243
244 @Override
245 public String toString() {
246 return String.format("Message.EditDocs { user: '%s', entry: %s }", user, entry);
247 }
248
249 }
250
251 public static final class MarkDeobf extends Message {
252
253 public final Entry<?> entry;
254
255 private MarkDeobf(String user, Entry<?> entry) {
256 super(user);
257 this.entry = entry;
258 }
259
260 @Override
261 public void write(DataOutput output) throws IOException {
262 super.write(output);
263 PacketHelper.writeEntry(output, entry);
264 }
265
266 @Override
267 public String translate() {
268 return String.format(I18n.translate("message.mark_deobf.text"), user, entry);
269 }
270
271 @Override
272 public Type getType() {
273 return Type.MARK_DEOBF;
274 }
275
276 @Override
277 public boolean equals(Object o) {
278 if (this == o) return true;
279 if (o == null || getClass() != o.getClass()) return false;
280 if (!super.equals(o)) return false;
281 MarkDeobf markDeobf = (MarkDeobf) o;
282 return Objects.equals(entry, markDeobf.entry);
283 }
284
285 @Override
286 public int hashCode() {
287 return Objects.hash(super.hashCode(), entry);
288 }
289
290 @Override
291 public String toString() {
292 return String.format("Message.MarkDeobf { user: '%s', entry: %s }", user, entry);
293 }
294
295 }
296
297 public static final class RemoveMapping extends Message {
298
299 public final Entry<?> entry;
300
301 private RemoveMapping(String user, Entry<?> entry) {
302 super(user);
303 this.entry = entry;
304 }
305
306 @Override
307 public void write(DataOutput output) throws IOException {
308 super.write(output);
309 PacketHelper.writeEntry(output, entry);
310 }
311
312 @Override
313 public String translate() {
314 return String.format(I18n.translate("message.remove_mapping.text"), user, entry);
315 }
316
317 @Override
318 public Type getType() {
319 return Type.REMOVE_MAPPING;
320 }
321
322 @Override
323 public boolean equals(Object o) {
324 if (this == o) return true;
325 if (o == null || getClass() != o.getClass()) return false;
326 if (!super.equals(o)) return false;
327 RemoveMapping that = (RemoveMapping) o;
328 return Objects.equals(entry, that.entry);
329 }
330
331 @Override
332 public int hashCode() {
333 return Objects.hash(super.hashCode(), entry);
334 }
335
336 @Override
337 public String toString() {
338 return String.format("Message.RemoveMapping { user: '%s', entry: %s }", user, entry);
339 }
340
341 }
342
343 public static final class Rename extends Message {
344
345 public final Entry<?> entry;
346 public final String newName;
347
348 private Rename(String user, Entry<?> entry, String newName) {
349 super(user);
350 this.entry = entry;
351 this.newName = newName;
352 }
353
354 @Override
355 public void write(DataOutput output) throws IOException {
356 super.write(output);
357 PacketHelper.writeEntry(output, entry);
358 PacketHelper.writeString(output, newName);
359 }
360
361 @Override
362 public String translate() {
363 return String.format(I18n.translate("message.rename.text"), user, entry, newName);
364 }
365
366 @Override
367 public Type getType() {
368 return Type.RENAME;
369 }
370
371 @Override
372 public boolean equals(Object o) {
373 if (this == o) return true;
374 if (o == null || getClass() != o.getClass()) return false;
375 if (!super.equals(o)) return false;
376 Rename rename = (Rename) o;
377 return Objects.equals(entry, rename.entry) &&
378 Objects.equals(newName, rename.newName);
379 }
380
381 @Override
382 public int hashCode() {
383 return Objects.hash(super.hashCode(), entry, newName);
384 }
385
386 @Override
387 public String toString() {
388 return String.format("Message.Rename { user: '%s', entry: %s, newName: '%s' }", user, entry, newName);
389 }
390
391 }
392
393}
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 00000000..86185536
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java
@@ -0,0 +1,22 @@
1package cuchaz.enigma.network;
2
3import java.net.Socket;
4
5public class ServerPacketHandler {
6
7 private final Socket client;
8 private final EnigmaServer server;
9
10 public ServerPacketHandler(Socket client, EnigmaServer server) {
11 this.client = client;
12 this.server = server;
13 }
14
15 public Socket getClient() {
16 return client;
17 }
18
19 public EnigmaServer getServer() {
20 return server;
21 }
22}
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 00000000..1b52cf14
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java
@@ -0,0 +1,59 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.translation.mapping.EntryMapping;
4import cuchaz.enigma.network.EnigmaServer;
5import cuchaz.enigma.network.Message;
6import cuchaz.enigma.network.ServerPacketHandler;
7import cuchaz.enigma.translation.representation.entry.Entry;
8import cuchaz.enigma.utils.Utils;
9
10import java.io.DataInput;
11import java.io.DataOutput;
12import java.io.IOException;
13
14public class ChangeDocsC2SPacket implements Packet<ServerPacketHandler> {
15 private Entry<?> entry;
16 private String newDocs;
17
18 ChangeDocsC2SPacket() {
19 }
20
21 public ChangeDocsC2SPacket(Entry<?> entry, String newDocs) {
22 this.entry = entry;
23 this.newDocs = newDocs;
24 }
25
26 @Override
27 public void read(DataInput input) throws IOException {
28 this.entry = PacketHelper.readEntry(input);
29 this.newDocs = PacketHelper.readString(input);
30 }
31
32 @Override
33 public void write(DataOutput output) throws IOException {
34 PacketHelper.writeEntry(output, entry);
35 PacketHelper.writeString(output, newDocs);
36 }
37
38 @Override
39 public void handle(ServerPacketHandler handler) {
40 EntryMapping mapping = handler.getServer().getMappings().getDeobfMapping(entry);
41
42 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
43 if (!valid) {
44 String oldDocs = mapping == null ? null : mapping.getJavadoc();
45 handler.getServer().sendPacket(handler.getClient(), new ChangeDocsS2CPacket(EnigmaServer.DUMMY_SYNC_ID, entry, oldDocs == null ? "" : oldDocs));
46 return;
47 }
48
49 if (mapping == null) {
50 mapping = new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName());
51 }
52 handler.getServer().getMappings().mapFromObf(entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs));
53
54 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
55 handler.getServer().sendToAllExcept(handler.getClient(), new ChangeDocsS2CPacket(syncId, entry, newDocs));
56 handler.getServer().sendMessage(Message.editDocs(handler.getServer().getUsername(handler.getClient()), entry));
57 }
58
59}
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 00000000..12a30253
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java
@@ -0,0 +1,44 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class ChangeDocsS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId;
13 private Entry<?> entry;
14 private String newDocs;
15
16 ChangeDocsS2CPacket() {
17 }
18
19 public ChangeDocsS2CPacket(int syncId, Entry<?> entry, String newDocs) {
20 this.syncId = syncId;
21 this.entry = entry;
22 this.newDocs = newDocs;
23 }
24
25 @Override
26 public void read(DataInput input) throws IOException {
27 this.syncId = input.readUnsignedShort();
28 this.entry = PacketHelper.readEntry(input);
29 this.newDocs = PacketHelper.readString(input);
30 }
31
32 @Override
33 public void write(DataOutput output) throws IOException {
34 output.writeShort(syncId);
35 PacketHelper.writeEntry(output, entry);
36 PacketHelper.writeString(output, newDocs);
37 }
38
39 @Override
40 public void handle(ClientPacketHandler controller) {
41 controller.changeDocs(new EntryReference<>(entry, entry.getName()), newDocs, false);
42 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
43 }
44}
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 00000000..78ef9645
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java
@@ -0,0 +1,33 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4
5import java.io.DataInput;
6import java.io.DataOutput;
7import java.io.IOException;
8
9public class ConfirmChangeC2SPacket implements Packet<ServerPacketHandler> {
10 private int syncId;
11
12 ConfirmChangeC2SPacket() {
13 }
14
15 public ConfirmChangeC2SPacket(int syncId) {
16 this.syncId = syncId;
17 }
18
19 @Override
20 public void read(DataInput input) throws IOException {
21 this.syncId = input.readUnsignedShort();
22 }
23
24 @Override
25 public void write(DataOutput output) throws IOException {
26 output.writeShort(syncId);
27 }
28
29 @Override
30 public void handle(ServerPacketHandler handler) {
31 handler.getServer().confirmChange(handler.getClient(), syncId);
32 }
33}
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 00000000..9a112a80
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java
@@ -0,0 +1,33 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ClientPacketHandler;
4
5import java.io.DataInput;
6import java.io.DataOutput;
7import java.io.IOException;
8
9public class KickS2CPacket implements Packet<ClientPacketHandler> {
10 private String reason;
11
12 KickS2CPacket() {
13 }
14
15 public KickS2CPacket(String reason) {
16 this.reason = reason;
17 }
18
19 @Override
20 public void read(DataInput input) throws IOException {
21 this.reason = PacketHelper.readString(input);
22 }
23
24 @Override
25 public void write(DataOutput output) throws IOException {
26 PacketHelper.writeString(output, reason);
27 }
28
29 @Override
30 public void handle(ClientPacketHandler controller) {
31 controller.disconnectIfConnected(reason);
32 }
33}
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 00000000..da0f44a5
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java
@@ -0,0 +1,75 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.EnigmaServer;
4import cuchaz.enigma.network.ServerPacketHandler;
5import cuchaz.enigma.network.Message;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10import java.util.Arrays;
11
12public class LoginC2SPacket implements Packet<ServerPacketHandler> {
13 private byte[] jarChecksum;
14 private char[] password;
15 private String username;
16
17 LoginC2SPacket() {
18 }
19
20 public LoginC2SPacket(byte[] jarChecksum, char[] password, String username) {
21 this.jarChecksum = jarChecksum;
22 this.password = password;
23 this.username = username;
24 }
25
26 @Override
27 public void read(DataInput input) throws IOException {
28 if (input.readUnsignedShort() != EnigmaServer.PROTOCOL_VERSION) {
29 throw new IOException("Mismatching protocol");
30 }
31 this.jarChecksum = new byte[EnigmaServer.CHECKSUM_SIZE];
32 input.readFully(jarChecksum);
33 this.password = new char[input.readUnsignedByte()];
34 for (int i = 0; i < password.length; i++) {
35 password[i] = input.readChar();
36 }
37 this.username = PacketHelper.readString(input);
38 }
39
40 @Override
41 public void write(DataOutput output) throws IOException {
42 output.writeShort(EnigmaServer.PROTOCOL_VERSION);
43 output.write(jarChecksum);
44 output.writeByte(password.length);
45 for (char c : password) {
46 output.writeChar(c);
47 }
48 PacketHelper.writeString(output, username);
49 }
50
51 @Override
52 public void handle(ServerPacketHandler handler) {
53 boolean usernameTaken = handler.getServer().isUsernameTaken(username);
54 handler.getServer().setUsername(handler.getClient(), username);
55 handler.getServer().log(username + " logged in with IP " + handler.getClient().getInetAddress().toString() + ":" + handler.getClient().getPort());
56
57 if (!Arrays.equals(password, handler.getServer().getPassword())) {
58 handler.getServer().kick(handler.getClient(), "disconnect.wrong_password");
59 return;
60 }
61
62 if (usernameTaken) {
63 handler.getServer().kick(handler.getClient(), "disconnect.username_taken");
64 return;
65 }
66
67 if (!Arrays.equals(jarChecksum, handler.getServer().getJarChecksum())) {
68 handler.getServer().kick(handler.getClient(), "disconnect.wrong_jar");
69 return;
70 }
71
72 handler.getServer().sendPacket(handler.getClient(), new SyncMappingsS2CPacket(handler.getServer().getMappings().getObfToDeobf()));
73 handler.getServer().sendMessage(Message.connect(username));
74 }
75}
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 00000000..a41c620f
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java
@@ -0,0 +1,48 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.translation.mapping.EntryMapping;
5import cuchaz.enigma.translation.representation.entry.Entry;
6import cuchaz.enigma.network.Message;
7
8import java.io.DataInput;
9import java.io.DataOutput;
10import java.io.IOException;
11
12public class MarkDeobfuscatedC2SPacket implements Packet<ServerPacketHandler> {
13 private Entry<?> entry;
14
15 MarkDeobfuscatedC2SPacket() {
16 }
17
18 public MarkDeobfuscatedC2SPacket(Entry<?> entry) {
19 this.entry = entry;
20 }
21
22 @Override
23 public void read(DataInput input) throws IOException {
24 this.entry = PacketHelper.readEntry(input);
25 }
26
27 @Override
28 public void write(DataOutput output) throws IOException {
29 PacketHelper.writeEntry(output, entry);
30 }
31
32 @Override
33 public void handle(ServerPacketHandler handler) {
34 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
35 if (!valid) {
36 handler.getServer().sendCorrectMapping(handler.getClient(), entry, true);
37 return;
38 }
39
40 handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName()));
41 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " marked " + entry + " as deobfuscated");
42
43 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
44 handler.getServer().sendToAllExcept(handler.getClient(), new MarkDeobfuscatedS2CPacket(syncId, entry));
45 handler.getServer().sendMessage(Message.markDeobf(handler.getServer().getUsername(handler.getClient()), entry));
46
47 }
48}
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 00000000..7504430d
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java
@@ -0,0 +1,40 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class MarkDeobfuscatedS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId;
13 private Entry<?> entry;
14
15 MarkDeobfuscatedS2CPacket() {
16 }
17
18 public MarkDeobfuscatedS2CPacket(int syncId, Entry<?> entry) {
19 this.syncId = syncId;
20 this.entry = entry;
21 }
22
23 @Override
24 public void read(DataInput input) throws IOException {
25 this.syncId = input.readUnsignedShort();
26 this.entry = PacketHelper.readEntry(input);
27 }
28
29 @Override
30 public void write(DataOutput output) throws IOException {
31 output.writeShort(syncId);
32 PacketHelper.writeEntry(output, entry);
33 }
34
35 @Override
36 public void handle(ClientPacketHandler controller) {
37 controller.markAsDeobfuscated(new EntryReference<>(entry, entry.getName()), false);
38 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
39 }
40}
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 00000000..3bc09e79
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java
@@ -0,0 +1,39 @@
1package cuchaz.enigma.network.packet;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
7import cuchaz.enigma.network.ServerPacketHandler;
8import cuchaz.enigma.network.Message;
9
10public class MessageC2SPacket implements Packet<ServerPacketHandler> {
11
12 private String message;
13
14 MessageC2SPacket() {
15 }
16
17 public MessageC2SPacket(String message) {
18 this.message = message;
19 }
20
21 @Override
22 public void read(DataInput input) throws IOException {
23 message = PacketHelper.readString(input);
24 }
25
26 @Override
27 public void write(DataOutput output) throws IOException {
28 PacketHelper.writeString(output, message);
29 }
30
31 @Override
32 public void handle(ServerPacketHandler handler) {
33 String message = this.message.trim();
34 if (!message.isEmpty()) {
35 handler.getServer().sendMessage(Message.chat(handler.getServer().getUsername(handler.getClient()), message));
36 }
37 }
38
39}
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 00000000..2b07968d
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java
@@ -0,0 +1,36 @@
1package cuchaz.enigma.network.packet;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
7import cuchaz.enigma.network.ClientPacketHandler;
8import cuchaz.enigma.network.Message;
9
10public class MessageS2CPacket implements Packet<ClientPacketHandler> {
11
12 private Message message;
13
14 MessageS2CPacket() {
15 }
16
17 public MessageS2CPacket(Message message) {
18 this.message = message;
19 }
20
21 @Override
22 public void read(DataInput input) throws IOException {
23 message = Message.read(input);
24 }
25
26 @Override
27 public void write(DataOutput output) throws IOException {
28 message.write(output);
29 }
30
31 @Override
32 public void handle(ClientPacketHandler handler) {
33 handler.addMessage(message);
34 }
35
36}
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 00000000..2f16dfb9
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/Packet.java
@@ -0,0 +1,15 @@
1package cuchaz.enigma.network.packet;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
7public interface Packet<H> {
8
9 void read(DataInput input) throws IOException;
10
11 void write(DataOutput output) throws IOException;
12
13 void handle(H handler);
14
15}
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 00000000..464606e0
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java
@@ -0,0 +1,135 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.translation.representation.MethodDescriptor;
4import cuchaz.enigma.translation.representation.TypeDescriptor;
5import cuchaz.enigma.translation.representation.entry.ClassEntry;
6import cuchaz.enigma.translation.representation.entry.Entry;
7import cuchaz.enigma.translation.representation.entry.FieldEntry;
8import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;
9import cuchaz.enigma.translation.representation.entry.MethodEntry;
10
11import java.io.DataInput;
12import java.io.DataOutput;
13import java.io.IOException;
14import java.nio.charset.StandardCharsets;
15
16public class PacketHelper {
17
18 private static final int ENTRY_CLASS = 0, ENTRY_FIELD = 1, ENTRY_METHOD = 2, ENTRY_LOCAL_VAR = 3;
19 private static final int MAX_STRING_LENGTH = 65535;
20
21 public static Entry<?> readEntry(DataInput input) throws IOException {
22 return readEntry(input, null, true);
23 }
24
25 public static Entry<?> readEntry(DataInput input, Entry<?> parent, boolean includeParent) throws IOException {
26 int type = input.readUnsignedByte();
27
28 if (includeParent && input.readBoolean()) {
29 parent = readEntry(input, null, true);
30 }
31
32 String name = readString(input);
33
34 String javadocs = null;
35 if (input.readBoolean()) {
36 javadocs = readString(input);
37 }
38
39 switch (type) {
40 case ENTRY_CLASS: {
41 if (parent != null && !(parent instanceof ClassEntry)) {
42 throw new IOException("Class requires class parent");
43 }
44 return new ClassEntry((ClassEntry) parent, name, javadocs);
45 }
46 case ENTRY_FIELD: {
47 if (!(parent instanceof ClassEntry)) {
48 throw new IOException("Field requires class parent");
49 }
50 TypeDescriptor desc = new TypeDescriptor(readString(input));
51 return new FieldEntry((ClassEntry) parent, name, desc, javadocs);
52 }
53 case ENTRY_METHOD: {
54 if (!(parent instanceof ClassEntry)) {
55 throw new IOException("Method requires class parent");
56 }
57 MethodDescriptor desc = new MethodDescriptor(readString(input));
58 return new MethodEntry((ClassEntry) parent, name, desc, javadocs);
59 }
60 case ENTRY_LOCAL_VAR: {
61 if (!(parent instanceof MethodEntry)) {
62 throw new IOException("Local variable requires method parent");
63 }
64 int index = input.readUnsignedShort();
65 boolean parameter = input.readBoolean();
66 return new LocalVariableEntry((MethodEntry) parent, index, name, parameter, javadocs);
67 }
68 default: throw new IOException("Received unknown entry type " + type);
69 }
70 }
71
72 public static void writeEntry(DataOutput output, Entry<?> entry) throws IOException {
73 writeEntry(output, entry, true);
74 }
75
76 public static void writeEntry(DataOutput output, Entry<?> entry, boolean includeParent) throws IOException {
77 // type
78 if (entry instanceof ClassEntry) {
79 output.writeByte(ENTRY_CLASS);
80 } else if (entry instanceof FieldEntry) {
81 output.writeByte(ENTRY_FIELD);
82 } else if (entry instanceof MethodEntry) {
83 output.writeByte(ENTRY_METHOD);
84 } else if (entry instanceof LocalVariableEntry) {
85 output.writeByte(ENTRY_LOCAL_VAR);
86 } else {
87 throw new IOException("Don't know how to serialize entry of type " + entry.getClass().getSimpleName());
88 }
89
90 // parent
91 if (includeParent) {
92 output.writeBoolean(entry.getParent() != null);
93 if (entry.getParent() != null) {
94 writeEntry(output, entry.getParent(), true);
95 }
96 }
97
98 // name
99 writeString(output, entry.getName());
100
101 // javadocs
102 output.writeBoolean(entry.getJavadocs() != null);
103 if (entry.getJavadocs() != null) {
104 writeString(output, entry.getJavadocs());
105 }
106
107 // type-specific stuff
108 if (entry instanceof FieldEntry) {
109 writeString(output, ((FieldEntry) entry).getDesc().toString());
110 } else if (entry instanceof MethodEntry) {
111 writeString(output, ((MethodEntry) entry).getDesc().toString());
112 } else if (entry instanceof LocalVariableEntry) {
113 LocalVariableEntry localVar = (LocalVariableEntry) entry;
114 output.writeShort(localVar.getIndex());
115 output.writeBoolean(localVar.isArgument());
116 }
117 }
118
119 public static String readString(DataInput input) throws IOException {
120 int length = input.readUnsignedShort();
121 byte[] bytes = new byte[length];
122 input.readFully(bytes);
123 return new String(bytes, StandardCharsets.UTF_8);
124 }
125
126 public static void writeString(DataOutput output, String str) throws IOException {
127 byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
128 if (bytes.length > MAX_STRING_LENGTH) {
129 throw new IOException("String too long, was " + bytes.length + " bytes, max " + MAX_STRING_LENGTH + " allowed");
130 }
131 output.writeShort(bytes.length);
132 output.write(bytes);
133 }
134
135}
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 00000000..3b8af81c
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java
@@ -0,0 +1,64 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ClientPacketHandler;
4import cuchaz.enigma.network.ServerPacketHandler;
5
6import java.util.HashMap;
7import java.util.Map;
8import java.util.function.Supplier;
9
10public class PacketRegistry {
11
12 private static final Map<Class<? extends Packet<ServerPacketHandler>>, Integer> c2sPacketIds = new HashMap<>();
13 private static final Map<Integer, Supplier<? extends Packet<ServerPacketHandler>>> c2sPacketCreators = new HashMap<>();
14 private static final Map<Class<? extends Packet<ClientPacketHandler>>, Integer> s2cPacketIds = new HashMap<>();
15 private static final Map<Integer, Supplier<? extends Packet<ClientPacketHandler>>> s2cPacketCreators = new HashMap<>();
16
17 private static <T extends Packet<ServerPacketHandler>> void registerC2S(int id, Class<T> clazz, Supplier<T> creator) {
18 c2sPacketIds.put(clazz, id);
19 c2sPacketCreators.put(id, creator);
20 }
21
22 private static <T extends Packet<ClientPacketHandler>> void registerS2C(int id, Class<T> clazz, Supplier<T> creator) {
23 s2cPacketIds.put(clazz, id);
24 s2cPacketCreators.put(id, creator);
25 }
26
27 static {
28 registerC2S(0, LoginC2SPacket.class, LoginC2SPacket::new);
29 registerC2S(1, ConfirmChangeC2SPacket.class, ConfirmChangeC2SPacket::new);
30 registerC2S(2, RenameC2SPacket.class, RenameC2SPacket::new);
31 registerC2S(3, RemoveMappingC2SPacket.class, RemoveMappingC2SPacket::new);
32 registerC2S(4, ChangeDocsC2SPacket.class, ChangeDocsC2SPacket::new);
33 registerC2S(5, MarkDeobfuscatedC2SPacket.class, MarkDeobfuscatedC2SPacket::new);
34 registerC2S(6, MessageC2SPacket.class, MessageC2SPacket::new);
35
36 registerS2C(0, KickS2CPacket.class, KickS2CPacket::new);
37 registerS2C(1, SyncMappingsS2CPacket.class, SyncMappingsS2CPacket::new);
38 registerS2C(2, RenameS2CPacket.class, RenameS2CPacket::new);
39 registerS2C(3, RemoveMappingS2CPacket.class, RemoveMappingS2CPacket::new);
40 registerS2C(4, ChangeDocsS2CPacket.class, ChangeDocsS2CPacket::new);
41 registerS2C(5, MarkDeobfuscatedS2CPacket.class, MarkDeobfuscatedS2CPacket::new);
42 registerS2C(6, MessageS2CPacket.class, MessageS2CPacket::new);
43 registerS2C(7, UserListS2CPacket.class, UserListS2CPacket::new);
44 }
45
46 public static int getC2SId(Packet<ServerPacketHandler> packet) {
47 return c2sPacketIds.get(packet.getClass());
48 }
49
50 public static Packet<ServerPacketHandler> createC2SPacket(int id) {
51 Supplier<? extends Packet<ServerPacketHandler>> creator = c2sPacketCreators.get(id);
52 return creator == null ? null : creator.get();
53 }
54
55 public static int getS2CId(Packet<ClientPacketHandler> packet) {
56 return s2cPacketIds.get(packet.getClass());
57 }
58
59 public static Packet<ClientPacketHandler> createS2CPacket(int id) {
60 Supplier<? extends Packet<ClientPacketHandler>> creator = s2cPacketCreators.get(id);
61 return creator == null ? null : creator.get();
62 }
63
64}
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 00000000..3f852285
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java
@@ -0,0 +1,55 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.translation.mapping.IllegalNameException;
5import cuchaz.enigma.translation.representation.entry.Entry;
6import cuchaz.enigma.network.Message;
7
8import java.io.DataInput;
9import java.io.DataOutput;
10import java.io.IOException;
11
12public class RemoveMappingC2SPacket implements Packet<ServerPacketHandler> {
13 private Entry<?> entry;
14
15 RemoveMappingC2SPacket() {
16 }
17
18 public RemoveMappingC2SPacket(Entry<?> entry) {
19 this.entry = entry;
20 }
21
22 @Override
23 public void read(DataInput input) throws IOException {
24 this.entry = PacketHelper.readEntry(input);
25 }
26
27 @Override
28 public void write(DataOutput output) throws IOException {
29 PacketHelper.writeEntry(output, entry);
30 }
31
32 @Override
33 public void handle(ServerPacketHandler handler) {
34 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
35
36 if (valid) {
37 try {
38 handler.getServer().getMappings().removeByObf(entry);
39 } catch (IllegalNameException e) {
40 valid = false;
41 }
42 }
43
44 if (!valid) {
45 handler.getServer().sendCorrectMapping(handler.getClient(), entry, true);
46 return;
47 }
48
49 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " removed the mapping for " + entry);
50
51 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
52 handler.getServer().sendToAllExcept(handler.getClient(), new RemoveMappingS2CPacket(syncId, entry));
53 handler.getServer().sendMessage(Message.removeMapping(handler.getServer().getUsername(handler.getClient()), entry));
54 }
55}
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 00000000..70d803c1
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java
@@ -0,0 +1,40 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class RemoveMappingS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId;
13 private Entry<?> entry;
14
15 RemoveMappingS2CPacket() {
16 }
17
18 public RemoveMappingS2CPacket(int syncId, Entry<?> entry) {
19 this.syncId = syncId;
20 this.entry = entry;
21 }
22
23 @Override
24 public void read(DataInput input) throws IOException {
25 this.syncId = input.readUnsignedShort();
26 this.entry = PacketHelper.readEntry(input);
27 }
28
29 @Override
30 public void write(DataOutput output) throws IOException {
31 output.writeShort(syncId);
32 PacketHelper.writeEntry(output, entry);
33 }
34
35 @Override
36 public void handle(ClientPacketHandler controller) {
37 controller.removeMapping(new EntryReference<>(entry, entry.getName()), false);
38 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
39 }
40}
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 00000000..e3e7e379
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java
@@ -0,0 +1,64 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.translation.mapping.IllegalNameException;
5import cuchaz.enigma.translation.mapping.EntryMapping;
6import cuchaz.enigma.translation.representation.entry.Entry;
7import cuchaz.enigma.network.Message;
8
9import java.io.DataInput;
10import java.io.DataOutput;
11import java.io.IOException;
12
13public class RenameC2SPacket implements Packet<ServerPacketHandler> {
14 private Entry<?> entry;
15 private String newName;
16 private boolean refreshClassTree;
17
18 RenameC2SPacket() {
19 }
20
21 public RenameC2SPacket(Entry<?> entry, String newName, boolean refreshClassTree) {
22 this.entry = entry;
23 this.newName = newName;
24 this.refreshClassTree = refreshClassTree;
25 }
26
27 @Override
28 public void read(DataInput input) throws IOException {
29 this.entry = PacketHelper.readEntry(input);
30 this.newName = PacketHelper.readString(input);
31 this.refreshClassTree = input.readBoolean();
32 }
33
34 @Override
35 public void write(DataOutput output) throws IOException {
36 PacketHelper.writeEntry(output, entry);
37 PacketHelper.writeString(output, newName);
38 output.writeBoolean(refreshClassTree);
39 }
40
41 @Override
42 public void handle(ServerPacketHandler handler) {
43 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
44
45 if (valid) {
46 try {
47 handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(newName));
48 } catch (IllegalNameException e) {
49 valid = false;
50 }
51 }
52
53 if (!valid) {
54 handler.getServer().sendCorrectMapping(handler.getClient(), entry, refreshClassTree);
55 return;
56 }
57
58 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " renamed " + entry + " to " + newName);
59
60 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
61 handler.getServer().sendToAllExcept(handler.getClient(), new RenameS2CPacket(syncId, entry, newName, refreshClassTree));
62 handler.getServer().sendMessage(Message.rename(handler.getServer().getUsername(handler.getClient()), entry, newName));
63 }
64}
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 00000000..787e89e6
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java
@@ -0,0 +1,48 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class RenameS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId;
13 private Entry<?> entry;
14 private String newName;
15 private boolean refreshClassTree;
16
17 RenameS2CPacket() {
18 }
19
20 public RenameS2CPacket(int syncId, Entry<?> entry, String newName, boolean refreshClassTree) {
21 this.syncId = syncId;
22 this.entry = entry;
23 this.newName = newName;
24 this.refreshClassTree = refreshClassTree;
25 }
26
27 @Override
28 public void read(DataInput input) throws IOException {
29 this.syncId = input.readUnsignedShort();
30 this.entry = PacketHelper.readEntry(input);
31 this.newName = PacketHelper.readString(input);
32 this.refreshClassTree = input.readBoolean();
33 }
34
35 @Override
36 public void write(DataOutput output) throws IOException {
37 output.writeShort(syncId);
38 PacketHelper.writeEntry(output, entry);
39 PacketHelper.writeString(output, newName);
40 output.writeBoolean(refreshClassTree);
41 }
42
43 @Override
44 public void handle(ClientPacketHandler controller) {
45 controller.rename(new EntryReference<>(entry, entry.getName()), newName, refreshClassTree, false);
46 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
47 }
48}
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 00000000..76ecbc7d
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java
@@ -0,0 +1,88 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.translation.mapping.EntryMapping;
4import cuchaz.enigma.translation.mapping.tree.EntryTree;
5import cuchaz.enigma.translation.mapping.tree.EntryTreeNode;
6import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
7import cuchaz.enigma.network.ClientPacketHandler;
8import cuchaz.enigma.network.EnigmaServer;
9import cuchaz.enigma.translation.representation.entry.Entry;
10
11import java.io.DataInput;
12import java.io.DataOutput;
13import java.io.IOException;
14import java.util.Collection;
15import java.util.List;
16import java.util.stream.Collectors;
17
18public class SyncMappingsS2CPacket implements Packet<ClientPacketHandler> {
19 private EntryTree<EntryMapping> mappings;
20
21 SyncMappingsS2CPacket() {
22 }
23
24 public SyncMappingsS2CPacket(EntryTree<EntryMapping> mappings) {
25 this.mappings = mappings;
26 }
27
28 @Override
29 public void read(DataInput input) throws IOException {
30 mappings = new HashEntryTree<>();
31 int size = input.readInt();
32 for (int i = 0; i < size; i++) {
33 readEntryTreeNode(input, null);
34 }
35 }
36
37 private void readEntryTreeNode(DataInput input, Entry<?> parent) throws IOException {
38 Entry<?> entry = PacketHelper.readEntry(input, parent, false);
39 EntryMapping mapping = null;
40 if (input.readBoolean()) {
41 String name = input.readUTF();
42 if (input.readBoolean()) {
43 String javadoc = input.readUTF();
44 mapping = new EntryMapping(name, javadoc);
45 } else {
46 mapping = new EntryMapping(name);
47 }
48 }
49 mappings.insert(entry, mapping);
50 int size = input.readUnsignedShort();
51 for (int i = 0; i < size; i++) {
52 readEntryTreeNode(input, entry);
53 }
54 }
55
56 @Override
57 public void write(DataOutput output) throws IOException {
58 List<EntryTreeNode<EntryMapping>> roots = mappings.getRootNodes().collect(Collectors.toList());
59 output.writeInt(roots.size());
60 for (EntryTreeNode<EntryMapping> node : roots) {
61 writeEntryTreeNode(output, node);
62 }
63 }
64
65 private static void writeEntryTreeNode(DataOutput output, EntryTreeNode<EntryMapping> node) throws IOException {
66 PacketHelper.writeEntry(output, node.getEntry(), false);
67 EntryMapping value = node.getValue();
68 output.writeBoolean(value != null);
69 if (value != null) {
70 PacketHelper.writeString(output, value.getTargetName());
71 output.writeBoolean(value.getJavadoc() != null);
72 if (value.getJavadoc() != null) {
73 PacketHelper.writeString(output, value.getJavadoc());
74 }
75 }
76 Collection<? extends EntryTreeNode<EntryMapping>> children = node.getChildNodes();
77 output.writeShort(children.size());
78 for (EntryTreeNode<EntryMapping> child : children) {
79 writeEntryTreeNode(output, child);
80 }
81 }
82
83 @Override
84 public void handle(ClientPacketHandler controller) {
85 controller.openMappings(mappings);
86 controller.sendPacket(new ConfirmChangeC2SPacket(EnigmaServer.DUMMY_SYNC_ID));
87 }
88}
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 00000000..b4a277a4
--- /dev/null
+++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java
@@ -0,0 +1,44 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ClientPacketHandler;
4
5import java.io.DataInput;
6import java.io.DataOutput;
7import java.io.IOException;
8import java.util.ArrayList;
9import java.util.List;
10
11public class UserListS2CPacket implements Packet<ClientPacketHandler> {
12
13 private List<String> users;
14
15 UserListS2CPacket() {
16 }
17
18 public UserListS2CPacket(List<String> users) {
19 this.users = users;
20 }
21
22 @Override
23 public void read(DataInput input) throws IOException {
24 int len = input.readUnsignedShort();
25 users = new ArrayList<>(len);
26 for (int i = 0; i < len; i++) {
27 users.add(input.readUTF());
28 }
29 }
30
31 @Override
32 public void write(DataOutput output) throws IOException {
33 output.writeShort(users.size());
34 for (String user : users) {
35 PacketHelper.writeString(output, user);
36 }
37 }
38
39 @Override
40 public void handle(ClientPacketHandler handler) {
41 handler.updateUserList(users);
42 }
43
44}