1 /*
2  * Copyright (c) 2017-2018 SEL
3  * 
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12  * See the GNU Lesser General Public License for more details.
13  * 
14  */
15 /**
16  * Copyright: 2017-2020 sel-project
17  * License: LGPL-3.0
18  * Authors: Kripth
19  * Source: $(HTTP github.com/sel-project/sel-server/sel/server/java.d, sel/server/java.d)
20  */
21 module sel.server.java;
22 
23 debug import core.thread : Thread;
24 
25 import std.algorithm : canFind, all;
26 import std.bitmanip : nativeToBigEndian;
27 import std.concurrency : spawn;
28 import std.conv : to;
29 import std.datetime : dur, usecs;
30 import std.datetime.stopwatch : StopWatch;
31 import std.json : JSONValue;
32 import std.socket : Address, Socket, TcpSocket, UdpSocket, SocketOptionLevel, SocketOption;
33 import std.uuid : UUID, randomUUID;
34 
35 import sel.net.modifiers : ModifierStream, LengthPrefixedStream, CompressedStream;
36 import sel.net.stream : Stream, TcpStream, UdpStream;
37 import sel.server.client : Client;
38 import sel.server.query : Query;
39 import sel.server.server;
40 
41 import sul.protocol.java340.status;
42 import sul.protocol.java340.login;
43 
44 import sul.utils.var : varuint;
45 
46 debug import std.stdio : writeln;
47 
48 public enum string[][uint] javaSupportedProtocols = [
49 	210u: ["1.10", "1.10.1", "1.10.2"],
50 	315u: ["1.11", "1.12"],
51 	316u: ["1.11.2"],
52 	335u: ["1.12"],
53 	338u: ["1.12.1"],
54 	340u: ["1.12.2"],
55 ];
56 
57 class JavaServer : GenericGameServer {
58 	
59 	public shared this(shared ServerInfo info, uint[] protocols=supportedProtocols.keys, shared Handler handler=new shared Handler()) {
60 		super(info, protocols, supportedProtocols.keys, handler);
61 	}
62 
63 	public shared this(shared ServerInfo info, shared Handler handler) {
64 		this(info, supportedProtocols.keys, handler);
65 	}
66 	
67 	public override shared pure nothrow @property @safe @nogc ushort defaultPort() {
68 		return ushort(25565);
69 	}
70 	
71 	/**
72 	 * Starts the server in a new thread.
73 	 */
74 	protected override shared void startImpl(Address address, shared Query query) {
75 		Socket socket = new TcpSocket(address.addressFamily);
76 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
77 		socket.blocking = true;
78 		socket.bind(address);
79 		socket.listen(10);
80 		spawn(&this.acceptClients, cast(shared)socket);
81 		if(query !is null) {
82 			Socket qsocket = new UdpSocket(address.addressFamily);
83 			qsocket.blocking = true;
84 			qsocket.bind(address);
85 			spawn(&this.acceptQueries, cast(shared)qsocket, query);
86 		}
87 	}
88 
89 	/**
90 	 * Starts an UDP socket that is only used for the external
91 	 * query protocol.
92 	 */
93 	protected shared void acceptQueries(shared Socket _socket, shared Query query) {
94 		debug Thread.getThis().name = "java_server@" ~ (cast()_socket).localAddress.toString() ~ "/accept_queries";
95 		UdpStream stream = new UdpStream(cast()_socket);
96 		Query.Handler handler;
97 		with(stream.socket.localAddress) handler = (cast()query).new Handler("MINECRAFT", toAddrString(), to!ushort(toPortString()));
98 		Address address;
99 		while(true) {
100 			ubyte[] buffer = stream.receiveFrom(address);
101 			if(buffer.length >= 2 && buffer[0] == 254 && buffer[1] == 253) {
102 				auto data = handler.handle(buffer[2..$]);
103 				if(data.length) {
104 					debug writeln(cast(string)data);
105 					stream.sendTo(data, address);
106 				}
107 			}
108 		}
109 	}
110 	
111 	/**
112 	 * Accepts new connection and handle in a new thread.
113 	 */
114 	protected shared void acceptClients(shared Socket _socket) {
115 		debug Thread.getThis().name = "java_server@" ~ (cast()_socket).localAddress.toString() ~ "/accept_clients";
116 		Socket socket = cast()_socket;
117 		while(true) {
118 			//Socket client = socket.accept();
119 			spawn(&this.handleNewClient, cast(shared)socket.accept());
120 		}
121 	}
122 	
123 	protected shared void handleNewClient(shared Socket _client) {
124 		Socket client = cast()_client;
125 		debug Thread.getThis().name = "java_client@" ~ client.remoteAddress.toString() ~ "/handle";
126 		client.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(8));
127 		ubyte[] buffer = new ubyte[96];
128 		auto recv = client.receive(buffer);
129 		if(recv > 0) {
130 			// valid packet received, handle
131 			if(buffer[0] == 254) {
132 				//TODO legacy ping
133 			} else {
134 				// read as a normal minecraft packet (length, id, payload)
135 				immutable length = varuint.fromBuffer(buffer);
136 				if(length != 0 && length < recv) {
137 					if(varuint.fromBuffer(buffer) == Handshake.ID) {
138 						Handshake handshake = Handshake.fromBuffer!false(buffer);
139 						auto stream = new LengthPrefixedStream!varuint(new TcpStream(client), buffer.length); // first packets should be pretty small
140 						if(handshake.next == Handshake.LOGIN) {
141 							// keep this thread for the player's session
142 							stream.maxLength = 1024;
143 							this.handleNewPlayer(stream, handshake);
144 						} else {
145 							// status
146 							ubyte[] request = stream.receive();
147 							if(request.length == 1 && request[0] == Request.ID) {
148 								stream.send(new Response(JSONValue(this.getPingResponse(client, handshake.protocol, handshake.serverAddress, handshake.serverPort)).toString()).encode());
149 								// handle optional latency calculation
150 								// connection timeout is still set to 8 seconds
151 								ubyte[] latency = stream.receive();
152 								if(latency.length == 9 && latency[0] == Latency.ID) {
153 									// send back the exact same packet
154 									stream.send(latency);
155 								}
156 								// blocking socket should be closed only after sending everything
157 							}
158 						}
159 					}
160 				}
161 			}
162 		}
163 		client.close();
164 	}
165 	
166 	/**
167 	 * Gets the JSON informations that the client will use to display the server
168 	 * in its servers list.
169 	 * This method can be overridden by custom implementations of the server.
170 	 */
171 	protected shared JSONValue[string] getPingResponse(Socket client, uint protocol, string ip, ushort port) {
172 		if(!this.protocols.canFind(protocol)) protocol = this.protocols[$-1];
173 		JSONValue[string] ret;
174 		ret["description"] = this.info.motd.java;
175 		ret["version"] = ["protocol": JSONValue(protocol), "name": JSONValue(javaSupportedProtocols[protocol][0])];
176 		ret["players"] = ["online": JSONValue(this.info.online), "max": JSONValue(this.info.max)];
177 		if(this.info.favicon.length) ret["favicon"] = this.info.favicon;
178 		return ret;
179 	}
180 	
181 	/**
182 	 * Handles a new player connection after the handshake packet with
183 	 * the status set to "login".
184 	 * At this state the socket should be blocking with the timeout set to 8 seconds.
185 	 */
186 	protected shared void handleNewPlayer(Stream stream, Handshake handshake) {
187 		// receive the login packet
188 		ubyte[] loginp = stream.receive();
189 		if(varuint.fromBuffer(loginp) == LoginStart.ID) {
190 			LoginStart login = LoginStart.fromBuffer!false(loginp);
191 			// start compression
192 			stream.send(new SetCompression(1024).encode());
193 			stream = new CompressedStream!varuint(stream, 1024);
194 			// perform validations
195 			immutable disconnect = this.validatePlayer(login.username, stream.socket.remoteAddress, handshake.protocol, handshake.serverAddress, handshake.serverPort);
196 			if(disconnect.length) {
197 				stream.send(new Disconnect(JSONValue(["text": disconnect]).toString()).encode());
198 				//stream.socket.close();
199 			} else {
200 				// send a login success
201 				//TODO encryption
202 				UUID uuid = randomUUID();
203 				stream.send(new LoginSuccess(uuid.toString(), login.username).encode());
204 				// start real session
205 				shared JavaClient session = new shared JavaClient(handshake.protocol, stream, login.username, uuid);
206 				this.onClientJoin(session);
207 				session.start(this); // blocking operation, returns when the session is closed
208 				this.onClientLeft(session);
209 			}
210 		}
211 	}
212 	
213 	protected shared string validatePlayer(string username, Address address, uint protocol, string usedIp, ushort usedPort) {
214 		if(!this.protocols.canFind(protocol)) return protocol > this.protocols[$-1] ? "Outdated Server!" : "Outdated Client!";
215 		if(username.length < 3 || username.length > 16 || !username.all!(a => a >= '0' && a <= '9' || a >= 'A' && a <= 'Z' || a >= 'a' && a <= 'z' || a == '_')) return "Invalid Username";
216 		return "";
217 	}
218 	
219 }
220 
221 class JavaClient : Client {
222 	
223 	private shared Stream stream;
224 
225 	private immutable ubyte serverboundKeepAliveId;
226 	private immutable ubyte clientboundKeepAliveId;
227 	private ubyte[] delegate(uint) encodeKeepAlive;
228 	
229 	public shared this(uint protocol, Stream stream, string username, UUID uuid) {
230 		super(JAVA, protocol, stream.socket.remoteAddress, username, uuid, VERSION_JAVA, javaSupportedProtocols[protocol][0]);
231 		this.stream = cast(shared)stream;
232 		this.serverboundKeepAliveId = getServerboundKeepAliveId(protocol);
233 		this.clientboundKeepAliveId = getClientboundKeepAliveId(protocol);
234 		this.encodeKeepAlive = getEncodeClientboundKeepAlive(protocol);
235 	}
236 	
237 	public shared void start(shared JavaServer server) {
238 		Stream stream = cast()this.stream;
239 		uint nextKeepAlive = 0;
240 		ubyte timeout = 0;
241 		StopWatch timer;
242 		timer.start();
243 		while(true) {
244 			ubyte[] recv = stream.receive();
245 			if(recv.length) {
246 				if(varuint.decode(recv, 0) == this.serverboundKeepAliveId) {
247 					//server.onLatencyUpdated(this, timer.peek().split!"msecs"().msecs); //TODO call on handler
248 				} else {
249 					// should never be compressed
250 					server.handler.onClientPacket(this, recv);
251 				}
252 			} else if(stream.lastRecv == 0 || ++timeout == 3) {
253 				// connection closed by the client or timed out
254 				//TODO call some kind of event
255 				stream.socket.close();
256 				break;
257 			}
258 			// check whether to send keep alive
259 			if(timer.peek() > usecs(15_000_000)) {
260 				timeout = 0;
261 				timer.reset();
262 				stream.send(this.clientboundKeepAliveId ~ this.encodeKeepAlive(++nextKeepAlive));
263 			}
264 		}
265 		timer.stop();
266 		// thread should be stopped automatically
267 	}
268 	
269 	/**
270 	 * Sends a game packet to the client.
271 	 */
272 	public override shared void send(ubyte[] packet) {
273 		//TODO do compression in another thread but maintain packet order
274 		(cast()this.stream).send(packet);
275 	}
276 	
277 	/**
278 	 * Sends a raw packet, without performing eventual compression
279 	 * or 0-padding.
280 	 */
281 	public override shared void directSend(ubyte[] payload) {
282 		(cast(ModifierStream)this.stream).stream.send(payload); // length is still appended
283 	}
284 
285 	protected override shared void disconnectImpl(string message, bool translation, string[] params) {
286 		JSONValue[string] reason;
287 		reason[translation ? "translate" : "text"] = message;
288 		if(params.length) {
289 			JSONValue[] rp;
290 			foreach(param ; params) rp ~= JSONValue(["text": param]);
291 			reason["with"] = rp;
292 		}
293 		Stream stream = cast()this.stream;
294 		stream.send(encodeClientboundDisconnect(this.protocol, JSONValue(reason).toString()));
295 		stream.socket.close();
296 		// let the client close the session when the packet has been received
297 	}
298 	
299 }
300 
301 private ubyte getServerboundKeepAliveId(uint protocol) {
302 	if(protocol >= 336) return 0x0B;
303 	else if(protocol >= 318) return 0x0C;
304 	else if(protocol >= 80) return 0x0B;
305 	else if(protocol >= 67) return 0x0A;
306 	else return 0x00;
307 }
308 
309 private ubyte getClientboundKeepAliveId(uint protocol) {
310 	if(protocol >= 332) return 0x1F;
311 	else if(protocol >= 318) return 0x20;
312 	else if(protocol >= 86) return 0x1F;
313 	else if(protocol >= 80) return 0x20;
314 	else if(protocol >= 67) return 0x1F;
315 	else return 0x00;
316 }
317 
318 private ubyte[] delegate(uint) getEncodeClientboundKeepAlive(uint protocol) {
319 	if(protocol >= 339) {
320 		// long
321 		return delegate ubyte[] (uint id){ return nativeToBigEndian!ulong(id).dup; };
322 	} else if(protocol >= 32) {
323 		// unsigned varint
324 		return delegate ubyte[] (uint id){ return varuint.encode(id); };
325 	} else {
326 		// int
327 		return delegate ubyte[] (uint id){ return nativeToBigEndian!uint(id).dup; };
328 	}
329 }
330 
331 private ubyte[] encodeClientboundDisconnect(uint protocol, string json) {
332 	ubyte getId() {
333 		if(protocol >= 332) return 0x1A;
334 		else if(protocol >= 318) return 0x1B;
335 		else if(protocol >= 80) return 0x1A;
336 		else return 0x19;
337 	}
338 	// id (varuint), json's length (varuint), json (bytes)
339 	return getId() ~ varuint.encode(json.length.to!uint) ~ cast(ubyte[])json;
340 }
341 
342 enum string[][uint] supportedProtocols = [
343 	4: ["1.7.1-pre", "1.7.2", "1.7.3-pre", "1.7.4", "1.7.5"],
344 	5: ["1.7.6", "1.7.7", "1.7.8", "1.7.9", "1.7.10", "14w02a"],
345 	6: ["14w03a"],
346 	7: ["14w04a"],
347 	8: ["14w04b"],
348 	9: ["14w05a"],
349 	10: ["14w06a"],
350 	11: ["14w07a"],
351 	12: ["14w08a"],
352 	14: ["14w11a"],
353 	15: ["14w17a"],
354 	16: ["14w18b"],
355 	17: ["14w19a"],
356 	18: ["14w20a"],
357 	19: ["14w21a"],
358 	20: ["14w21b"],
359 	21: ["14w25a"],
360 	22: ["14w25b"],
361 	23: ["14w26a"],
362 	24: ["14w26b"],
363 	25: ["14w26c"],
364 	26: ["14w27a", "14w27b"],
365 	27: ["14w28a"],
366 	28: ["14w28b"],
367 	29: ["14w29a"],
368 	30: ["14w30a"],
369 	31: ["14w30c"],
370 	32: ["14w31a"],
371 	33: ["14w32a"],
372 	34: ["14w32b"],
373 	35: ["14w32c"],
374 	36: ["14w32d"],
375 	37: ["14w33a"],
376 	38: ["14w33b"],
377 	39: ["14w33c"],
378 	40: ["14w34a"],
379 	41: ["14w34b"],
380 	42: ["14w34c"],
381 	43: ["14w34d"],
382 	44: ["1.8-pre1"],
383 	45: ["1.8-pre2"],
384 	46: ["1.8-pre3"],
385 	47: ["1.8", "1.8.1", "1.8.2", "1.8.3", "1.8.4", "1.8.5", "1.8.6", "1.8.7", "1.8.8", "1.8.9"],
386 	48: ["15w14a"],
387 	49: ["15w31a"],
388 	50: ["15w31b"],
389 	51: ["15w31c"],
390 	52: ["15w32a"],
391 	53: ["15w32b"],
392 	54: ["15w32c"],
393 	55: ["15w33a"],
394 	56: ["15w33b"],
395 	57: ["15w33c"],
396 	58: ["15w34a"],
397 	59: ["15w34b"],
398 	60: ["15w34c"],
399 	61: ["15w34d"],
400 	62: ["15w35a"],
401 	63: ["15w35b"],
402 	64: ["15w35c"],
403 	65: ["15w35d"],
404 	66: ["15w35e"],
405 	67: ["15w36a"],
406 	68: ["15w36b"],
407 	69: ["15w36c"],
408 	70: ["15w36d"],
409 	71: ["15w37a"],
410 	72: ["15w38a"],
411 	73: ["15w38b"],
412 	74: ["15w39c"],
413 	75: ["15w40a"],
414 	76: ["15w40b"],
415 	77: ["15w41a"],
416 	78: ["15w41b"],
417 	79: ["15w42a"],
418 	80: ["15w43a"],
419 	81: ["15w43b"],
420 	82: ["15w43c"],
421 	83: ["15w44a"],
422 	84: ["15w44b"],
423 	85: ["15w45a"],
424 	86: ["15w46a"],
425 	87: ["15w47a"],
426 	88: ["15w47b"],
427 	89: ["15w47c"],
428 	90: ["15w49a"],
429 	91: ["15w49b"],
430 	92: ["15w50a"],
431 	93: ["15w51a"],
432 	94: ["15w51b"],
433 	95: ["16w02a"],
434 	96: ["16w03a"],
435 	97: ["16w04a"],
436 	98: ["16w05a"],
437 	99: ["16w05b"],
438 	100: ["16w06a"],
439 	101: ["16w07a"],
440 	102: ["16w07b"],
441 	103: ["1.9-pre1"],
442 	104: ["1.9-pre2"],
443 	105: ["1.9-pre3"],
444 	106: ["1.9-pre4"],
445 	107: ["1.9"],
446 	108: ["1.9.1-pre2"],
447 	109: ["1.9.2", "16w14a", "16w15a", "16w15b", "1.9.3-pre1", "1.9.3-pre3", "1.9.3", "1.9.4"],
448 	110: ["1.9.3-pre2"],
449 	201: ["16w20a"],
450 	202: ["16w21a"],
451 	203: ["16w21b"],
452 	204: ["1.10-pre1"],
453 	205: ["1.10-pre2"],
454 	210: ["1.10", "1.10.1", "1.10.2"],
455 	301: ["16w32a"],
456 	302: ["16w32b"],
457 	303: ["16w33a"],
458 	304: ["16w35a"],
459 	305: ["16w36a"],
460 	306: ["16w38a"],
461 	307: ["16w39a"],
462 	308: ["16w39b"],
463 	309: ["16w39c"],
464 	310: ["16w40a"],
465 	311: ["16w41a"],
466 	312: ["16w42a"],
467 	313: ["16w43a", "16w44a"],
468 	314: ["1.11-pre1"],
469 	315: ["1.11"],
470 	316: ["16w50a", "1.11.1", "1.11.2"],
471 	317: ["17w06a"],
472 	318: ["17w13a"],
473 	319: ["17w13b"],
474 	320: ["17w14a"],
475 	321: ["17w15a"],
476 	322: ["17w16a"],
477 	323: ["17w16b"],
478 	324: ["17w17a"],
479 	325: ["17w17b"],
480 	326: ["17w18a"],
481 	327: ["17w18b"],
482 	328: ["1.12-pre1"],
483 	329: ["1.12-pre2"],
484 	330: ["1.12-pre3"],
485 	331: ["1.12-pre4"],
486 	332: ["1.12-pre5"],
487 	333: ["1.12-pre6"],
488 	334: ["1.12-pre7"],
489 	335: ["1.12"],
490 	336: ["17w31a"],
491 	337: ["1.12.1-pre1"],
492 	338: ["1.12.1"],
493 	339: ["1.12.2-pre1", "1.12.2-pre2"],
494 	340: ["1.12.2"],
495 	341: ["17w43a"],
496 	342: ["17w43b"],
497 	343: ["17w45a"],
498 	344: ["17w45b"],
499 	345: ["17w46a"],
500 	346: ["17w47a"],
501 	347: ["17w47b"],
502 	348: ["17w48a"],
503 	349: ["17w49a"],
504 	350: ["17w49b"],
505 	351: ["17w50a"],
506 	352: ["18w01a"],
507 	353: ["18w02a"],
508 	354: ["18w03a"],
509 	355: ["18w03b"],
510 ];