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/bedrock.d, sel/server/bedrock.d)
20  */
21 module sel.server.bedrock;
22 
23 import core.thread : Thread;
24 
25 import std.algorithm : sort, canFind, all, max;
26 import std.base64 : Base64, Base64Impl, Base64Exception;
27 import std.bitmanip : peek;
28 import std.concurrency : spawn, Tid, sendc = send, receiveTimeout, receiveOnly;
29 import std.conv : to, ConvException;
30 import std.datetime : dur;
31 import std.datetime.stopwatch : StopWatch;
32 import std.json : JSONValue, JSON_TYPE, parseJSON, JSONException;
33 import std.socket : Address, AddressFamily, InternetAddress, Internet6Address, Socket, UdpSocket, SocketOptionLevel, SocketOption;
34 import std.string : indexOf, lastIndexOf, startsWith, split, join;
35 import std.system : Endian;
36 import std.typecons : Tuple;
37 import std.uuid : UUID, parseUUID, UUIDParsingException;
38 import std.zlib : Compress, UnCompress, ZlibException;
39 
40 import sel.net.stream : UdpStream, RaknetStream;
41 import sel.server.client : Client, InputMode;
42 import sel.server.query : Query;
43 import sel.server.server;
44 
45 import sul.protocol.raknet8.encapsulated : ClientConnect, ServerHandshake, ClientHandshake, ClientCancelConnection, ConnectedPing = Ping, ConnectedPong = Pong;
46 import sul.protocol.raknet8.types : RaknetAddress = Address;
47 import sul.protocol.raknet8.unconnected;
48 import sul.utils.var : varuint;
49 
50 debug import std.stdio : writeln;
51 
52 enum __magic = cast(ubyte[16])x"00 FF FF 00 FE FE FE FE FD FD FD FD 12 34 56 78";
53 
54 public enum string[][uint] bedrockSupportedProtocols = [
55 	137u: ["1.2.0", "1.2.1", "1.2.2", "1.2.3"],
56 	141u: ["1.2.5"],
57 	150u: ["1.2.6"],
58 	160u: ["1.2.7", "1.2.8", "1.2.9"],
59 ];
60 
61 abstract class BedrockServer : GenericGameServer {
62 
63 	public shared this(shared ServerInfo info, uint[] protocols, uint[] supportedProtocols, shared Handler handler) {
64 		super(info, protocols, supportedProtocols, handler);
65 	}
66 	
67 	public override shared pure nothrow @property @safe @nogc ushort defaultPort() {
68 		return ushort(19132);
69 	}
70 
71 }
72 
73 alias BedrockServerImpl(string[][uint] supportedProtocols) = BedrockServerImpl!(supportedProtocols.keys);
74 
75 template BedrockServerImpl(uint[] rawSupportedProtocols) if(checkProtocols(rawSupportedProtocols, bedrockSupportedProtocols.keys).length) {
76 
77 	enum supportedProtocols = checkProtocols(rawSupportedProtocols, bedrockSupportedProtocols.keys);
78 
79 	class BedrockServerImpl : BedrockServer {
80 	
81 		public shared this(shared ServerInfo info, uint[] protocols=supportedProtocols, shared Handler handler=new shared Handler()) {
82 			super(info, protocols, supportedProtocols, handler);
83 		}
84 		
85 		public shared this(shared ServerInfo info, shared Handler handler, uint[] protocols=supportedProtocols) {
86 			this(info, protocols, handler);
87 		}
88 		
89 		protected override shared void startImpl(Address address, shared Query query) {
90 			Socket socket = new UdpSocket(address.addressFamily);
91 			socket.blocking = true;
92 			socket.bind(address);
93 			auto shared_socket = cast(shared)socket;
94 			if(address.addressFamily == AddressFamily.INET) {
95 				new Thread({ try { this.receivePackets!true(shared_socket, handler, query); } catch(Throwable t){ debug{writeln(t);} }}).start();
96 			} else {
97 				assert(0, "Unsupported address family: " ~ address.addressFamily.to!string);
98 			}
99 		}
100 		
101 		protected shared void receivePackets(bool ipv4)(shared Socket socket, shared Handler handler, shared Query query) {
102 			debug Thread.getThis().name = "bedrock_server.receive_packets";
103 			auto handlerThread = cast(shared)spawn(&this.startHandler); // used to handle packets instead of doing it in the socket/uncompression thread
104 			auto compressionManager = cast(shared)spawn(&this.startCompressionManager);
105 			immutable protocolsString = to!string(this.protocols[$-1]) ~ ";" ~ bedrockSupportedProtocols[this.protocols[$-1]][0];
106 			static if(ipv4) {
107 				alias Id = Tuple!(uint, ushort);
108 			} else {
109 				alias Id = Tuple!(ubyte[16], ushort);
110 			}
111 			UdpStream stream = new UdpStream(cast()socket);
112 			immutable bool acceptQueries = query !is null;
113 			Query.Handler qhandler;
114 			if(acceptQueries) {
115 				with(stream.socket.localAddress) qhandler = (cast()query).new Handler("MINECRAFTPE", toAddrString(), to!ushort(toPortString()));
116 			}
117 			shared(RaknetSession)[Id] sessions;
118 			Address _address;
119 			while(true) {
120 				ubyte[] buffer = stream.receiveFrom(_address);
121 				if(buffer.length) {
122 					static if(ipv4) {
123 						InternetAddress address = cast(InternetAddress)_address;
124 					} else {
125 						Internet6Address address = cast(Internet6Address)_address;
126 					}
127 					Id id = Id(address.addr, address.port);
128 					auto session = id in sessions;
129 					if(session && !(*session).closed) {
130 						(*session).handle(buffer);
131 					} else {
132 						// not a session
133 						switch(buffer[0]) {
134 							case Ping.ID:
135 								Ping ping = Ping.fromBuffer(buffer);
136 								stream.sendTo(new Pong(ping.pingId, 0, __magic, "MCPE;" ~ this.info.motd.bedrock ~ ";" ~ protocolsString ~ ";" ~ to!string(this.info.online) ~ ";" ~ to!string(this.info.max)).encode(), address);
137 								break;
138 							case OpenConnectionRequest1.ID:
139 								auto packet = OpenConnectionRequest1.fromBuffer(buffer);
140 								if(packet.mtu.length > 448) {
141 									// do not allow connection when mtu is too small
142 									stream.sendTo(new OpenConnectionReply1(__magic, 0, false, cast(ushort)packet.mtu.length).encode(), address);
143 									// session is not created yet
144 								}
145 								break;
146 							case OpenConnectionRequest2.ID:
147 								auto packet = OpenConnectionRequest2.fromBuffer(buffer);
148 								if(packet.mtuLength > 448 && packet.mtuLength < 1536) {
149 									stream.sendTo(new OpenConnectionReply2(__magic, 0, createAddress(address), packet.mtuLength, false).encode(), address);
150 									// every packet for this session is not encapsulated
151 									sessions[id] = new shared RaknetSession(this.protocols, address, new RaknetStream(stream.socket, address, packet.mtuLength), handler, handlerThread, compressionManager);
152 								}
153 								break;
154 							default:
155 								if(acceptQueries && buffer.length >= 2 && buffer[0] == 254 && buffer[1] == 253) {
156 									auto data = qhandler.handle(buffer[2..$]);
157 									if(data.length) {
158 										stream.sendTo(data, address);
159 									}
160 								}
161 								break;
162 						}
163 					}
164 				}
165 			}
166 		}
167 		
168 		private shared void startHandler() {
169 			debug Thread.getThis().name = "bedrock_server.handler";
170 			while(true) {
171 				try {
172 					auto data = receiveOnly!(shared Client, immutable(ubyte)[])();
173 					data[0].handler(data[1].dup);
174 				} catch(Throwable t) {
175 					debug writeln(t);
176 				}
177 			}
178 		}
179 
180 		private shared void startCompressionManager() {
181 			debug Thread.getThis().name = "bedrock_server.compression_manager";
182 			while(true) {
183 				try {
184 					auto client = receiveOnly!(shared BedrockClient)();
185 					client.startThreads();
186 				} catch(Throwable t) {
187 					debug writeln(t);
188 				}
189 			}
190 		}
191 		
192 	}
193 
194 	class RaknetSession {
195 		
196 		private immutable(uint)[] protocols;
197 		
198 		private shared Address address;
199 		private shared RaknetStream stream;
200 		
201 		private shared Handler handler;
202 		public shared Tid handlerThread;
203 		public shared Tid compressionManager;
204 		
205 		private shared bool _closed = false;
206 		
207 		private shared void delegate(ubyte[]) shared handleFunction;
208 		
209 		private BedrockClient client;
210 		
211 		public shared this(immutable(uint)[] protocols, Address address, RaknetStream stream, shared Handler handler, shared Tid handlerThread, shared Tid compressionManager) {
212 			this.protocols = protocols;
213 			stream.acceptSplit = false;
214 			this.address = cast(shared)address;
215 			this.stream = cast(shared)stream;
216 			this.handler = handler;
217 			this.handlerThread = handlerThread;
218 			this.compressionManager = compressionManager;
219 			this.handleFunction = &this.handleClientConnect;
220 		}
221 		
222 		public shared nothrow @property @safe @nogc bool closed() {
223 			return this._closed;
224 		}
225 		
226 		public shared void handle(ubyte[] buffer) {
227 			buffer = (cast()this.stream).handle(buffer);
228 			if(buffer.length) {
229 				switch(buffer[0]) {
230 					case ConnectedPing.ID:
231 						(cast()this.stream).send(new ConnectedPong(ConnectedPing.fromBuffer(buffer).time).encode());
232 						//TODO use to calculate latency
233 						break;
234 					case ClientCancelConnection.ID:
235 						this.close();
236 						break;
237 					default:
238 						this.handleFunction(buffer);
239 				}
240 			}
241 		}
242 		
243 		private shared void handleClientConnect(ubyte[] buffer) {
244 			if(buffer[0] == ClientConnect.ID) {
245 				auto packet = ClientConnect.fromBuffer(buffer);
246 				auto stream = cast()this.stream;
247 				stream.send(new ServerHandshake(createAddress(cast()this.address), cast(ushort)stream.mtu, cast()systemAddresses, packet.pingId, 0).encode());
248 				this.handleFunction = &this.handleClientHandshake;
249 			}
250 		}
251 		
252 		private shared void handleClientHandshake(ubyte[] buffer) {
253 			if(buffer[0] == ClientHandshake.ID) {
254 				auto packet = ClientHandshake.fromBuffer(buffer);
255 				(cast()this.stream).acceptSplit = true;
256 				this.handleFunction = &this.handleLogin;
257 			}
258 		}
259 		
260 		private shared void handleLogin(ubyte[] buffer) {
261 			switch(buffer[0]) {
262 				case 254:
263 					// 0.15, 1.0, 1.1, 1.2 (container)
264 					if(buffer.length > 6) {
265 						this.handleFunction = &this.handleNothing; // avoid handling the login more than once
266 						if(buffer[1] == 0x78) {
267 							// compressed (1.1, 1.2)
268 							// uncompress
269 							// do protocol controls
270 							// handle non-compressed login body
271 							spawn(&this.handleCompressedBody, buffer[1..$].idup);
272 							break;
273 						} else if(buffer[1] == 1 || buffer[1] == 6) {
274 							// login or batch packet (1.0)
275 							(cast()this.stream).send(cast(ubyte[])[254, 2, 0, 0, 0, 1]);
276 						}
277 					}
278 					this.close();
279 					break;
280 				case 142:
281 					// 0.14 (container)
282 					(cast()this.stream).send(cast(ubyte[])[142, 144, 0, 0, 0, 1]);
283 					this.close();
284 					break;
285 				case 143:
286 				case 146:
287 					// 0.12, 0.13 (login and batch)
288 					(cast()this.stream).send(cast(ubyte[])[144, 0, 0, 0, 1]);
289 					this.close();
290 					break;
291 				case 177:
292 				case 130:
293 					// 0.11 (login)
294 					// 0.8, 0.9, 0.10 (login)
295 					(cast()this.stream).send(cast(ubyte[])[131, 0, 0, 0, 1]);
296 					this.close();
297 					break;
298 				default:
299 					this.close();
300 					break;
301 			}
302 		}
303 		
304 		private shared void handleCompressedBody(immutable(ubyte)[] payload) {
305 			debug Thread.getThis().name = "bedrock_client@?";
306 			ubyte[][] packets;
307 			try {
308 				packets = uncompressPackets(payload);
309 			} catch(ZlibException) {
310 				this.close();
311 				return;
312 			}
313 			if(packets.length == 1 && packets[0].length > 5 && packets[0][0] == 1) {
314 				ubyte[] login = packets[0];
315 				immutable protocol = this.validateProtocol(login[1..5]);
316 				if(protocol != 0) {
317 					this.handleLoginBody(protocol, login[5..$].idup);
318 					return;
319 				}
320 			}
321 			// wrong packet or wrong protocol
322 			this.close();
323 		}
324 		
325 		private shared void handleLoginBody(uint protocol, immutable(ubyte)[] _payload) {
326 			ubyte[] payload = _payload.dup;
327 			immutable edition = (){
328 				if(protocol < 8 || protocol >= 120) {
329 					// vanilla by default
330 					return 0;
331 				} else {
332 					immutable ret = payload[0];
333 					payload = payload[1..$];
334 					return ret;
335 				}
336 			}();
337 			if(varuint.fromBuffer(payload) == payload.length && payload.length) {
338 				size_t index = 0;
339 				string readBody() {
340 					if(index + 4 < payload.length) {
341 						immutable length = peek!(uint, Endian.littleEndian)(payload, &index);
342 						if(length + index <= payload.length) {
343 							return cast(string)payload[index..index+=length];
344 						}
345 					}
346 					return "";
347 				}
348 				JSONValue chainJSON;
349 				try chainJSON = parseJSON(readBody()); // {"chain":["a.b.c"]}
350 				catch(JSONException) {}
351 				if(chainJSON.type == JSON_TYPE.OBJECT) {
352 					auto chain = "chain" in chainJSON;
353 					if(chain && chain.type == JSON_TYPE.ARRAY && chain.array.length && chain.array.length <= 3 && chain.array.all!(a => a.type == JSON_TYPE.STRING)) {
354 						try chainJSON = parseJWT(chain.array[$-1].str);
355 						catch(JSONException) return this.close();
356 						if(chainJSON.type == JSON_TYPE.OBJECT) {
357 							auto extraData = "extraData" in chainJSON;
358 							if(extraData && extraData.type == JSON_TYPE.OBJECT) {
359 								auto displayName = "displayName" in *extraData;
360 								auto identity = "identity" in *extraData;
361 								if(displayName && identity && displayName.type == JSON_TYPE.STRING && identity.type == JSON_TYPE.STRING) {
362 									UUID uuid;
363 									try uuid = parseUUID(identity.str);
364 									catch(UUIDParsingException) return this.close();
365 									JSONValue clientData;
366 									try clientData = parseJWT(readBody());
367 									catch(JSONException) return this.close();
368 									if(clientData.type == JSON_TYPE.OBJECT) {
369 										auto gameVersion = "GameVersion" in clientData.object;
370 										this.client = (){
371 											final switch(protocol) {
372 												foreach(p ; TupleOf!supportedProtocols) {
373 													case p:
374 													return cast(shared BedrockClient)new shared BedrockClientOf!p(this, protocol, displayName.str, uuid, gameVersion && (*gameVersion).type == JSON_TYPE.STRING ? (*gameVersion).str : "");
375 												}
376 											}
377 										}();
378 										//TODO validate username
379 										this.handleFunction = &this.handlePlay;
380 										this.client.parseClientData(clientData.object);
381 										this.handler.onClientJoin(this.client);
382 										return;
383 									}
384 								}
385 							}
386 						}
387 					}
388 				}
389 			}
390 			// generic failure
391 			this.close();
392 		}
393 		
394 		private shared void handlePlay(ubyte[] buffer) {
395 			if(buffer.length > 1 && buffer[0] == 254) {
396 				this.client.handle(buffer[1..$]);
397 			}
398 		}
399 		
400 		private shared void handleNothing(ubyte[] buffer) {}
401 		
402 		/**
403 		 * Returns: the number of the protocol indicated by the client if accepted by the server or 0
404 		 */
405 		private shared uint validateProtocol(ubyte[] data) {
406 			uint protocol = peek!uint(data, 0);
407 			ubyte[] packet = cast(ubyte[])[2, 0, 0, 0, 0, 0, 0]; // id (byte), padding (byte[2]), code (int)
408 			if(!this.protocols.canFind(protocol)) {
409 				if(protocol > this.protocols[$-1]) packet[$-1] = 2; // outdated server
410 				else packet[$-1] = 1; // outdated client
411 				//this.close();
412 				protocol = 0;
413 			}
414 			// compress everything! (since protocol 110)
415 			Compress compress = new Compress(1);
416 			packet = cast(ubyte[])compress.compress(varuint.encode(packet.length.to!uint) ~ packet);
417 			(cast()this.stream).send(ubyte(254) ~ packet);
418 			return protocol;
419 		}
420 		
421 		/**
422 		 * Removes the session.
423 		 */
424 		private shared void close() {
425 			if(!this._closed) {
426 				this._closed = true;
427 				this.handleFunction = &this.handleNothing; // do not handle anymore
428 				if(this.client !is null) {
429 					this.client.stopThreads();
430 					this.handler.onClientLeft(this.client);
431 				}
432 			}
433 		}
434 		
435 	}
436 
437 	class BedrockClient : Client {
438 		
439 		protected shared RaknetSession raknetSession;
440 
441 		public shared Tid uncompression, compression;
442 		
443 		public shared this(uint protocol, shared RaknetSession session, string username, UUID uuid, string gameVersion) {
444 			auto versions = bedrockSupportedProtocols[protocol];
445 			string gv;
446 			if(versions.canFind(gameVersion)) {
447 				gv = gameVersion;
448 			} else {
449 				// may be a beta
450 				foreach(version_ ; versions) {
451 					if(gameVersion.startsWith(version_)) {
452 						gv = version_;
453 						break;
454 					}
455 				}
456 			}
457 			super(BEDROCK, protocol, cast()session.address, username, uuid, VERSION_MINECRAFT, gv.length ? gv : versions[0]); //TODO edu mode
458 			this.raknetSession = session;
459 			sendc(cast()raknetSession.compressionManager, this);
460 		}
461 
462 		public shared void startThreads() {
463 			this.uncompression = cast(shared)spawn(&this.startUncompression);
464 			this.compression = cast(shared)spawn(&this.startCompression);
465 		}
466 		
467 		public shared void stopThreads() {
468 			// crashes the threads
469 			sendc(cast()this.uncompression, "");
470 			sendc(cast()this.compression, "");
471 		}
472 		
473 		public shared void parseClientData(JSONValue[string] json) {
474 			void parse(string index, JSON_TYPE type, void delegate(JSONValue) success) {
475 				auto ret = index in json;
476 				if(ret && ret.type == type) {
477 					success(*ret);
478 					json.remove(index);
479 				}
480 			}
481 			parse("SkinId", JSON_TYPE.STRING, (JSONValue value){
482 				skinName = value.str;
483 			});
484 			parse("SkinData", JSON_TYPE.STRING, (JSONValue value){
485 				//TODO check length
486 				try skinData = Base64.decode(value.str).idup;
487 				catch(Base64Exception) {}
488 			});
489 			parse("SkinGeometryName", JSON_TYPE.STRING, (JSONValue value){
490 				skinGeometryName = value.str;
491 			});
492 			parse("SkinGeometry", JSON_TYPE.STRING, (JSONValue value){
493 				try skinGeometryData = Base64.decode(value.str).idup;
494 				catch(Base64Exception) {}
495 			});
496 			parse("CapeData", JSON_TYPE.STRING, (JSONValue value){
497 				try skinCape = Base64.decode(value.str).idup;
498 				catch(Base64Exception) {}
499 			});
500 			parse("CurrentInputMode", JSON_TYPE.INTEGER, (JSONValue value){
501 				inputMode = (){
502 					switch(value.integer) {
503 						case 0: return InputMode.controller;
504 						case 1: return InputMode.touch;
505 						default: return InputMode.keyboard;
506 					}
507 				}();
508 			});
509 			parse("LanguageCode", JSON_TYPE.STRING, (JSONValue value){
510 				language = value.str;
511 			});
512 			parse("ServerAddress", JSON_TYPE.STRING, (JSONValue value){
513 				auto spl = value.str.split(":");
514 				if(spl.length >= 2) {
515 					serverIp = spl[0..$-1].join(":");
516 					try serverPort = to!ushort(spl[$-1]);
517 					catch(ConvException) {}
518 				}
519 			});
520 			this.gameData = JSONValue(json);
521 		}
522 
523 		public shared void handle(ubyte[] buffer) {
524 			sendc(cast()this.uncompression, buffer.idup);
525 		}
526 
527 		private shared void startUncompression() {
528 			while(true) {
529 				foreach(packet ; uncompressPackets(receiveOnly!(immutable(ubyte)[])())) {
530 					//sendc(cast()this.raknetSession.handlerThread, cast(shared Client)this, packet.idup);
531 					//TODO handle in another thread
532 					this.raknetSession.handler.onClientPacket(cast(shared)this, packet);
533 				}
534 			}
535 		}
536 		
537 		/**
538 		 * Sends a game packet to the client.
539 		 */
540 		public override shared synchronized void send(ubyte[] packet) {
541 			//writeln("sending ", packet[0]);
542 			// compress body in another thread but maintain order
543 			sendc(cast()this.compression, packet.idup);
544 		}
545 		
546 		public override shared synchronized void directSend(ubyte[] payload) {
547 			// assuming that the content has already been compressed
548 			(cast()this.raknetSession.stream).send(ubyte(254) ~ payload);
549 		}
550 
551 		public override shared void disconnectImpl(string message, bool translation, string[] params) {
552 			this.send(this.createDisconnect(message));
553 			this.raknetSession.close();
554 		}
555 		
556 		protected abstract shared ubyte[] createDisconnect(string message);
557 		
558 		private shared void startCompression() {
559 			while(true) {
560 				ubyte[][] data;
561 				while(receiveTimeout(dur!"msecs"(0), (immutable(ubyte)[] payload){ data ~= payload.dup; }, (string close){ throw new Exception(""); })) {}
562 				if(data.length) {
563 					sendData(data);
564 				}
565 			}
566 		}
567 		
568 		protected shared void sendData(ubyte[][] packets) {
569 			// always compress the body
570 			foreach(packet ; this.compressPackets(packets)) {
571 				(cast()this.raknetSession.stream).send(ubyte(254) ~ packet);
572 			}
573 		}
574 
575 		//TODO do not compress too much data
576 		protected shared ubyte[][] compressPackets(ubyte[][] packets) {
577 			//writeln("compressing ", packets.length, " toghether (", totalLength(packets), ")");
578 			ubyte[][] compressed = new ubyte[][1];
579 			size_t length = 0;
580 			foreach(packet ; packets) {
581 				compressed[$-1] ~= varuint.encode(packet.length.to!uint + 2); // 2 bytes of padding
582 				compressed[$-1] ~= packet[0];
583 				compressed[$-1] ~= [ubyte(0), ubyte(0)]; // 2-bytes padding
584 				compressed[$-1] ~= packet[1..$];
585 				if((length += packet.length) > 500_000) {
586 					// do not compress more than 500 MB
587 					compressed.length++;
588 					length = 0;
589 				}
590 			}
591 			foreach(ref buffer ; compressed) {
592 				Compress compress = new Compress();
593 				buffer = cast(ubyte[])compress.compress(buffer);
594 				buffer ~= cast(ubyte[])compress.flush();
595 			}
596 			return compressed;
597 		}
598 		
599 	}
600 
601 	enum uint[uint] same = [
602 		141u: 160u,
603 		150u: 160u,
604 	];
605 
606 	template BedrockClientOf(uint __protocol) {
607 		
608 		static if(same.keys.canFind(__protocol)) {
609 			
610 			// they're exactly the same in therm of packets used in
611 			// the software, may be different in other packets.
612 			alias BedrockClientOf = BedrockClientOf!(same[__protocol]);
613 			
614 		} else {
615 			
616 			mixin("import Play = sul.protocol.bedrock" ~ __protocol.to!string ~ ".play;");
617 			
618 			class BedrockClientOf : BedrockClient {
619 				
620 				public shared this(shared RaknetSession session, uint protocol, string username, UUID uuid, string gameVersion) {
621 					super(protocol, session, username, uuid, gameVersion);
622 				}
623 				
624 				public override shared ubyte[] createDisconnect(string message) {
625 					return new Play.Disconnect(false, message).encode();
626 				}
627 				
628 				static if(is(typeof(Session.batchId))) {
629 					
630 					protected override shared pure nothrow @property @safe @nogc ubyte batchId() {
631 						return Play.Batch.ID;
632 					}
633 					
634 				}
635 				
636 			}
637 			
638 		}
639 		
640 	}
641 
642 }
643 
644 private JSONValue parseJWT(string data) {
645 	immutable a = data.indexOf(".");
646 	if(a != -1) {
647 		immutable z = data.lastIndexOf(".");
648 		if(a != z) {
649 			try return parseJSON(cast(string)Base64Impl!('-', '_', Base64.NoPadding).decode(data[a+1..z]));
650 			catch(Base64Exception) {}
651 		}
652 	}
653 	return JSONValue.init;
654 }
655 
656 private ubyte[][] uncompressPackets(inout(ubyte)[] payload) {
657 	UnCompress uc = new UnCompress();
658 	auto data = cast(ubyte[])uc.uncompress(payload);
659 	data ~= cast(ubyte[])uc.flush();
660 	ubyte[][] packets;
661 	size_t index, length;
662 	while((length = varuint.decode(data, &index)) >= 3 && length <= data.length - index) {
663 		// packets have a 2-bytes padding after the id
664 		packets ~= (data[index..index+1] ~ data[index+3..index+length]);
665 		index += length;
666 	}
667 	return packets;
668 }
669 
670 private __gshared RaknetAddress[10] systemAddresses;
671 
672 shared static this() {
673 	foreach(ref address ; systemAddresses) {
674 		address.type = 4;
675 	}
676 }
677 
678 RaknetAddress createAddress(Address address) {
679 	RaknetAddress ret;
680 	auto v4 = cast(InternetAddress)address;
681 	if(v4) {
682 		ret.type = 4;
683 		ret.ipv4 = v4.addr ^ uint.max;
684 		ret.port = v4.port;
685 	} else {
686 		auto v6 = cast(Internet6Address)address;
687 		assert(v6 !is null);
688 		ret.type = 6;
689 		ret.ipv6 = v6.addr; //TODO mask with 0xff
690 		ret.port = v6.port;
691 	}
692 	return ret;
693 }
694 
695 private size_t totalLength(ubyte[][] packets) {
696 	size_t length = 0;
697 	foreach(packet ; packets) {
698 		length += packet.length;
699 	}
700 	return length;
701 }
702 
703 unittest {
704 
705 	alias Server = BedrockServerImpl!bedrockSupportedProtocols;
706 
707 }