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 }