1 /* 2 * Copyright (c) 2017-2018 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: 2017-2018 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/selery/source/selery/hub/server.d, selery/hub/server.d) 28 */ 29 module selery.hub.server; 30 31 import core.atomic : atomicOp; 32 import core.cpuid; 33 import core.sys.posix.signal; 34 import core.thread; 35 36 import std.algorithm : sort, canFind; 37 import std.array : Appender; 38 import std.ascii : newline; 39 import std.base64 : Base64; 40 import std.bitmanip : nativeToBigEndian; 41 import std.conv : to; 42 import std.file; 43 import std.json; 44 import std.math : round; 45 import std.net.curl : download, CurlException; 46 import std.random : uniform; 47 import std.regex : replaceAll, ctRegex; 48 import std.socket : Address, InternetAddress, Internet6Address, AddressFamily; 49 import std.string : join, split, toLower, strip, indexOf, replace, startsWith; 50 import std.system : endian; 51 import std.typecons; 52 import std.uuid : parseUUID, UUID; 53 import std.utf : UTFException; 54 55 import arsd.terminal : Terminal, ConsoleOutputType; 56 57 import imageformats : ImageIOException, read_png_header_from_mem; 58 59 import myip : privateAddresses, publicAddress4; 60 61 import sel.hncom.login : HubInfo, NodeInfo; 62 import sel.hncom.status : Log; 63 import sel.server.client : Client; 64 import sel.server.query : Query; 65 import sel.server.util : ServerInfo, PlayerHandler = Handler; 66 67 import selery.about; 68 import selery.config : Config; 69 import selery.hub.handler.handler : Handler; 70 import selery.hub.handler.hncom : AbstractNode; 71 import selery.hub.handler.rcon : RconClient; 72 import selery.hub.handler.webadmin : WebAdminClient; 73 import selery.hub.player : PlayerSession; 74 import selery.lang : Translation; 75 import selery.log : Format, Message, Logger; 76 import selery.plugin : Plugin; 77 import selery.server : Server; 78 import selery.util.portable : startWebAdmin; 79 import selery.util.thread; 80 import selery.util.util : milliseconds; 81 82 /+version(Windows) { 83 84 import core.sys.windows.wincon : CTRL_C_EVENT; 85 import core.sys.windows.windef : DWORD, BOOL; 86 87 alias extern (Windows) BOOL function(DWORD) PHANDLER_ROUTINE; 88 extern (Windows) BOOL SetConsoleCtrlHandler(PHANDLER_ROUTINE, BOOL); 89 90 extern (Windows) int sigHandler(uint sig) { 91 if(sig == CTRL_C_EVENT) { 92 Server.instance.shutdown(); 93 return true; // this will let the process run in background until it kills himself 94 } 95 return false; // windows will instantly kill the process 96 } 97 98 } else version(Posix) { 99 100 import core.sys.posix.signal; 101 102 extern (C) void extsig(int sig) { 103 Server.instance.shutdown(); 104 //server.stop(); 105 } 106 107 }+/ 108 109 struct Icon { 110 111 string url; 112 113 ubyte[] data; 114 string base64data; 115 116 static Icon fromData(void[] _data) { 117 ubyte[] data = cast(ubyte[])_data; 118 return Icon("", data, "data:image/png;base64," ~ Base64.encode(data).idup); 119 } 120 121 static Icon fromURL(string url, void[] data) { 122 auto ret = fromData(data); 123 ret.url = url; 124 return ret; 125 } 126 127 } 128 129 class HubServer : PlayerHandler, Server { 130 131 public immutable bool lite; 132 133 public immutable ulong id; 134 private shared ulong uuid_count; 135 136 private immutable ulong started; 137 138 private shared Config _config; 139 private shared ServerLogger _logger; 140 private shared const(AddressRange)[] _accepted_nodes; 141 private shared Icon _icon; 142 private shared ServerInfo _info; 143 private shared Query _query; 144 145 private shared Plugin[] _plugins; 146 147 private shared uint n_max = 0; //TODO replace with _info.max 148 149 private shared uint n_upload, n_download; 150 151 private shared Handler handler; 152 153 private shared AbstractNode[uint] nodes; 154 private shared AbstractNode[] main_nodes; 155 private shared AbstractNode[string] nodesNames; 156 private shared size_t[string] n_plugins; 157 158 private shared WebAdminClient[uint] webAdmins; 159 private shared RconClient[uint] rcons; 160 161 private shared PlayerSession[uint] _players; 162 163 public shared this(bool lite, Config config, Plugin[] plugins=[], string[] args=[]) { 164 165 assert(config.files !is null); 166 assert(config.lang !is null); 167 assert(config.hub !is null); 168 169 debug Thread.getThis().name = "hub_server"; 170 171 this.lite = lite; 172 173 this._info = new shared ServerInfo(); 174 if(config.hub.query) { 175 this._query = new shared Query(this._info); 176 this._query.software = Software.name ~ " " ~ Software.displayVersion; 177 } 178 179 AddressRange[] acceptedNodes; 180 foreach(node ; config.hub.acceptedNodes) { 181 acceptedNodes ~= AddressRange.parse(node); 182 } 183 this._accepted_nodes = cast(shared const)acceptedNodes; 184 185 Terminal terminal = Terminal(ConsoleOutputType.linear); 186 187 terminal.setTitle(config.hub.displayName ~ " | " ~ (!lite ? "hub | " : "") ~ Software.simpleDisplay); 188 189 Message[][] errors = this.load(config); 190 191 this._logger = cast(shared)new ServerLogger(this, &terminal); 192 193 this.logger.log(Translation("startup.starting", [Format.green ~ Software.name ~ Format.reset ~ " " ~ Format.white ~ Software.fullVersion ~ Format.reset ~ " " ~ Software.fullCodename])); 194 195 static if(!__supported) { 196 this.logger.logWarning(Translation("startup.unsupported", [Software.name])); 197 } 198 199 // print error message from config loading 200 foreach(message ; errors) { 201 this.logger.logMessage(message); 202 } 203 204 this.id = uniform!"[]"(ulong.min, ulong.max); 205 this.uuid_count = uniform!"[]"(ulong.min, ulong.max); 206 207 auto pr = privateAddresses; 208 if(pr.length) this.logger.log("Private addresses: ", pr.join(", ")); 209 immutable pu4 = publicAddress4; 210 if(pu4.length) this.logger.log("Public address: ", pu4); 211 212 this.handler = new shared Handler(this, this._info, this._query); 213 214 /*version(Windows) { 215 SetConsoleCtrlHandler(&sigHandler, true); 216 } else version(linux) { 217 sigset(SIGTERM, &extsig); 218 sigset(SIGINT, &extsig); 219 }*/ 220 221 //TODO load plugins and their language files 222 223 // open web admin GUI 224 if(config.hub.webAdminOpen) { 225 import std.process : Pid; 226 Pid pid = null; 227 foreach(address ; config.hub.webAdminAddresses) { 228 if(address.ip == "127.0.0.1" || address.ip == "::1") { 229 pid = startWebAdmin(address.port); 230 break; 231 } 232 } 233 if(pid is null && config.hub.webAdminAddresses.length) { 234 pid = startWebAdmin(config.hub.webAdminAddresses[0].port); 235 } 236 } 237 238 this.started = milliseconds; 239 240 if(!this.lite) this.logger.log(Translation("startup.started")); 241 242 int last_online, last_max = this.maxPlayers; 243 size_t next_analytics = 0; 244 while(true) { 245 uint online = this.onlinePlayers.to!uint; 246 if(online != last_online || this.maxPlayers != last_max) { 247 last_online = online; 248 last_max = this.maxPlayers; 249 foreach(node ; this.nodes) { 250 node.updatePlayers(last_online, last_max); 251 } 252 } 253 Thread.sleep(dur!"msecs"(1000)); 254 } 255 256 } 257 258 /** 259 * Loads the configuration file. 260 * - validates the motds 261 * - validates protocols 262 * - loads and validate favicon 263 * - validate accepted language(s) 264 * - load languages 265 */ 266 private shared Message[][] load(ref Config config) { 267 Message[][] errors; 268 // MOTDs and protocols 269 this._info.motd.raw = config.hub.displayName; 270 if(config.hub.bedrock) with(config.hub.bedrock) { 271 motd = motd.replaceAll(ctRegex!"&([0-9a-zk-or])", "§$1"); 272 motd = motd.replace(";", ""); 273 motd ~= Format.reset; 274 this._info.motd.bedrock = motd; 275 validateProtocols(protocols, supportedBedrockProtocols, supportedBedrockProtocols); 276 } 277 if(config.hub.java) with(config.hub.java) { 278 motd = motd.replaceAll(ctRegex!"&([0-9a-zk-or])", "§$1"); 279 motd = motd.replace("\\n", "\n"); 280 this._info.motd.java = motd; 281 validateProtocols(protocols, supportedJavaProtocols, supportedJavaProtocols); 282 } 283 // icon 284 Icon icon; 285 if(exists(config.hub.favicon) && isFile(config.hub.favicon)) { 286 icon = Icon.fromData(read(config.hub.favicon)); 287 } else if(config.hub.favicon.startsWith("http://") || config.hub.favicon.startsWith("https://")) { 288 immutable cached = "icon_" ~ Base64.encode(cast(ubyte[])config.hub.favicon).idup; 289 if(!config.files.hasTemp(cached)) { 290 try { 291 static import std.net.curl; 292 std.net.curl.download(config.hub.favicon, config.files.temp ~ cached); 293 } catch(CurlException e) { 294 errors ~= Message.convert(Format.yellow, Translation("warning.iconFailed", config.hub.favicon, e.msg)); 295 } 296 } 297 if(config.files.hasTemp(cached)) { 298 icon = Icon.fromURL(config.hub.favicon, config.files.readTemp(cached)); 299 } 300 } 301 if(icon.data.length) { 302 bool valid = false; 303 try { 304 auto header = read_png_header_from_mem(icon.data); 305 if(header.width == 64 && header.height == 64) valid = true; 306 } catch(ImageIOException) {} 307 if(!valid) { 308 errors ~= Message.convert(Format.yellow, Translation("warning.invalidIcon", config.hub.favicon)); 309 icon = Icon.init; 310 } 311 } 312 this._icon = cast(shared)icon; 313 this._info.favicon = this._icon.base64data; 314 // save new config 315 this._config = cast(shared)config; 316 return errors; 317 } 318 319 public shared void shutdown() { 320 this.handler.shutdown(); 321 foreach(node ; this.nodes) node.onClosed(false); 322 import core.stdc.stdlib : exit; 323 this.logger.log("Shutting down"); 324 exit(0); 325 } 326 327 public shared nothrow @property UUID nextUUID() { 328 ubyte[16] data = nativeToBigEndian(this.id) ~ nativeToBigEndian(this.uuid_count); 329 atomicOp!"+="(this.uuid_count, 1); 330 return UUID(data); 331 } 332 333 public shared nothrow @property @trusted @nogc ulong nextPool() { 334 ulong pool = this.uuid_count; 335 atomicOp!"+="(this.uuid_count, uint.max); 336 return pool; 337 } 338 339 /** 340 * Gets the server's uptime in milliseconds. 341 */ 342 public shared @property @safe const uint uptime() { 343 return cast(uint)(milliseconds - this.started); 344 } 345 346 /** 347 * Gets the server's configuration. 348 */ 349 public override shared nothrow @property @trusted @nogc const(Config) config() { 350 return cast()this._config; 351 } 352 353 public override shared @property Logger logger() { 354 return cast()this._logger; 355 } 356 357 public override shared pure nothrow @property @trusted @nogc const(Plugin)[] plugins() { 358 return cast(const(Plugin)[])this._plugins; 359 } 360 361 public final shared nothrow @property @safe @nogc shared(ServerInfo) info() { 362 return this._info; 363 } 364 365 public shared nothrow @property @trusted @nogc const(Icon) icon() { 366 return cast()this._icon; 367 } 368 369 /// ditto 370 public shared nothrow @property @safe @nogc const uint upload() { 371 return this.n_upload; 372 } 373 374 /// ditto 375 public shared nothrow @property @safe @nogc const uint download() { 376 return this.n_download; 377 } 378 379 /** 380 * Gets the number of online players. 381 */ 382 public shared nothrow @property @safe @nogc const uint onlinePlayers() { 383 version(X86_64) { 384 return cast(uint)this._players.length; 385 } else { 386 return this._players.length; 387 } 388 } 389 390 /** 391 * Gets the number of max players. 392 */ 393 public shared nothrow @property @safe @nogc const int maxPlayers() { 394 return this.n_max; 395 } 396 397 public shared @property @safe @nogc void updateMaxPlayers() { 398 int max = 0; 399 foreach(node ; this.nodes) { 400 if(node.max == NodeInfo.UNLIMITED) { 401 this.n_max = HubInfo.UNLIMITED; 402 return; 403 } else { 404 max += node.max; 405 } 406 } 407 this.n_max = max; 408 } 409 410 /** 411 * Indicates whether the server is full. 412 */ 413 public shared @property @safe @nogc const(bool) full() { 414 if(this.maxPlayers == HubInfo.UNLIMITED) return false; 415 foreach(node ; this.nodes) { 416 if(!node.full) return false; 417 } 418 return true; 419 } 420 421 /** 422 * Gets the online players. 423 */ 424 public shared @property shared(PlayerSession[]) players() { 425 return this._players.values; 426 } 427 428 /** 429 * Handles a command. 430 */ 431 public shared void handleCommand(string command, ubyte origin, Address sender, int commandId) { 432 shared AbstractNode recv; 433 if(this.lite) { 434 recv = this.nodes.values[0]; 435 } else { 436 string name = ""; 437 immutable space = command.indexOf(" "); 438 if(space != -1) { 439 name = command[0..space]; 440 command = command[space..$].strip; 441 if(command.length == 0) return; 442 } 443 recv = this.nodeByName(name); 444 if(recv is null) return; //TODO print error message 445 } 446 recv.remoteCommand(command, origin, sender, commandId); 447 } 448 449 /** 450 * Handles a log. 451 */ 452 public shared void handleLog(string node, Log.Message[] messages, ulong timestamp, int commandId, int worldId, string worldName) { 453 Message[] log; 454 if(node.length) log ~= Message("[node/" ~ node ~ "]"); 455 if(worldName.length) log ~= Message("[world/" ~ worldName ~ "]"); 456 if(log.length) log ~= Message(" "); 457 // convert from Log.Message[] to Message[] 458 foreach(message ; messages) { 459 if(message.translation) log ~= Message(Translation(message.message, message.params)); 460 else log ~= Message(message.message); 461 } 462 (cast()this._logger).logWith(log, commandId, worldId); 463 } 464 465 public shared bool acceptNode(Address address) { 466 if(this.config.hub.maxNodes != 0) { 467 if(this.nodes.length >= this.config.hub.maxNodes) return false; 468 } 469 // check if it's an IPv4-mapped in IPv6 470 if(cast(Internet6Address)address) { 471 auto v6 = cast(Internet6Address)address; 472 ubyte[16] bytes = v6.addr; 473 if(bytes[10] == 255 && bytes[11] == 255) { // ::ffff:127.0.0.1 474 address = new InternetAddress(to!string(bytes[12]) ~ "." ~ to!string(bytes[13]) ~ "." ~ to!string(bytes[14]) ~ "." ~ to!string(bytes[15]), v6.port); 475 } 476 } 477 foreach(ar ; this._accepted_nodes) { 478 if((cast()ar).contains(address)) return true; 479 } 480 return false; 481 } 482 483 public shared nothrow @property @safe @nogc bool hasNodes() { 484 return this.nodes.length != 0; 485 } 486 487 /** 488 * Returns: the first main node which is not full 489 */ 490 public shared nothrow @property @safe @nogc shared(AbstractNode) mainNode() { 491 foreach(node ; this.main_nodes) { 492 if(node.main && (node.max == NodeInfo.UNLIMITED || node.online < node.max)) return node; 493 } 494 return null; 495 } 496 497 public shared nothrow @property @safe shared(AbstractNode)[] mainNodes() { 498 shared AbstractNode[] nodes; 499 foreach(node ; this.main_nodes) { 500 if(node.main && (node.max == NodeInfo.UNLIMITED || node.online < node.max)) nodes ~= node; 501 } 502 return nodes; 503 } 504 505 public shared nothrow shared(AbstractNode) nodeByName(string name) { 506 auto ptr = name in this.nodesNames; 507 return ptr ? *ptr : null; 508 } 509 510 public shared nothrow shared(AbstractNode) nodeById(uint id) { 511 auto ptr = id in this.nodes; 512 return ptr ? *ptr : null; 513 } 514 515 public shared @property string[] nodeNames() { 516 return this.nodesNames.keys; 517 } 518 519 public shared @property shared(AbstractNode[]) nodesList() { 520 return this.nodes.values; 521 } 522 523 public synchronized shared void add(shared AbstractNode node) { 524 if(!this.lite) this.logger.log(Format.green, "+ ", Format.reset, node.toString()); 525 this.nodes[node.id] = node; 526 this.nodesNames[node.name] = node; 527 // update players 528 this.updateMaxPlayers(); 529 // add to main, if main 530 if(node.main) this.main_nodes ~= node; 531 // add plugins 532 foreach(plugin ; node.plugins) { 533 string str = plugin.name ~ " " ~ plugin.version_; 534 if(str in this.n_plugins) { 535 atomicOp!"+="(this.n_plugins[str], 1); 536 } else { 537 this.n_plugins[str] = 1; 538 //TODO add to _query.plugins 539 } 540 } 541 // notify other nodes 542 foreach(shared AbstractNode on ; this.nodes) { 543 on.addNode(node); 544 } 545 } 546 547 public synchronized shared void remove(shared AbstractNode node) { 548 this.logger.log(Format.red, "- ", Format.reset, node.toString()); 549 this.nodes.remove(node.id); 550 this.nodesNames.remove(node.name); 551 // update players 552 this.updateMaxPlayers(); 553 // remove from main, if main 554 if(node.main) { 555 foreach(i, n; this.main_nodes) { 556 if(n.id == node.id) { 557 this.main_nodes = this.main_nodes[0..i] ~ this.main_nodes[i+1..$]; 558 break; 559 } 560 } 561 } 562 // remove plugins 563 foreach(plugin ; node.plugins) { 564 string str = plugin.name ~ " " ~ plugin.version_; 565 auto ptr = str in this.n_plugins; 566 if(ptr) { 567 atomicOp!"-="(*ptr, 1); 568 if(*ptr == 0) { 569 this.n_plugins.remove(str); 570 //TODO remove from _query.plugins 571 } 572 } 573 } 574 // notify other nodes 575 foreach(shared AbstractNode on ; this.nodes) { 576 on.removeNode(node); 577 } 578 } 579 580 public override shared void onClientJoin(shared Client client) { 581 auto player = new shared PlayerSession(this, client); 582 if(player.firstConnect()) this._players[player.id] = player; 583 } 584 585 public override shared void onClientLeft(shared Client client) { 586 auto player = client.id in this._players; 587 if(player) { 588 this._players.remove(client.id); 589 (*player).onClosed(); // remove from the node 590 } 591 } 592 593 public override shared void onClientPacket(shared Client client, ubyte[] packet) { 594 auto player = client.id in this._players; 595 if(player) { 596 (*player).sendToNode(packet); 597 } 598 } 599 600 public shared void onBedrockClientRequestChunkRadius(shared Client client, uint viewDistance) { 601 //TODO select player and update if changed (the node will send the confirmation back) 602 } 603 604 public shared void onJavaClientClientSettings(shared Client client, string language, ubyte viewDistance, uint chatMode, bool chatColors, ubyte skinParts, uint mainHand) { 605 //TODO select player and update if changed 606 } 607 608 public synchronized shared void add(WebAdminClient webAdmin) { 609 this.logger.log(Format.green, "+ ", Format.reset, webAdmin.toString()); 610 this.webAdmins[webAdmin.id] = cast(shared)webAdmin; 611 } 612 613 public synchronized shared void remove(WebAdminClient webAdmin) { 614 if(this.webAdmins.remove(webAdmin.id)) { 615 this.logger.log(Format.red, "- ", Format.reset, webAdmin.toString()); 616 } 617 } 618 619 public synchronized shared void add(shared RconClient rcon) { 620 this.logger.log(Format.green, "+ ", Format.reset, rcon.toString()); 621 this.rcons[rcon.id] = rcon; 622 } 623 624 public synchronized shared void remove(shared RconClient rcon) { 625 if(this.rcons.remove(rcon.id)) { 626 this.logger.log(Format.red, "- ", Format.reset, rcon.toString()); 627 } 628 } 629 630 public shared nothrow shared(PlayerSession) playerFromId(immutable(uint) id) { 631 auto ptr = id in this._players; 632 return ptr ? *ptr : null; 633 } 634 635 public shared shared(PlayerSession) playerFromIdentifier(ubyte[] idf) { 636 foreach(shared PlayerSession player ; this.players) { 637 if(player.iusername == idf) return player; 638 } 639 return null; 640 } 641 642 } 643 644 private class ServerLogger : Logger { 645 646 private shared HubServer server; 647 648 public this(shared HubServer server, Terminal* terminal) { 649 super(terminal, server.lang); 650 this.server = server; 651 } 652 653 protected override void logImpl(Message[] messages) { 654 this.logWith(messages, Log.NO_COMMAND, Log.NO_WORLD); 655 } 656 657 public void logWith(Message[] messages, int commandId, int worldId) { 658 super.logImpl(messages); 659 if(this.server.rcons.length) { 660 Appender!string appender; 661 foreach(message ; messages) { 662 final switch(message.type) { 663 case Message.FORMAT: 664 appender.put(cast(string)message.format); 665 break; 666 case Message.TEXT: 667 appender.put(message.text); 668 break; 669 case Message.TRANSLATION: 670 appender.put(this.server.lang.translate(message.translation.translatable.default_, message.translation.parameters)); 671 break; 672 } 673 } 674 immutable log = appender.data; 675 foreach(rcon ; this.server.rcons) { 676 rcon.consoleMessage(log, commandId); 677 } 678 } 679 foreach(webAdmin ; this.server.webAdmins) { 680 (cast()webAdmin).sendLog(messages, commandId, worldId); 681 } 682 } 683 684 } 685 686 /** 687 * Stores a range of ip addresses. 688 */ 689 struct AddressRange { 690 691 /** 692 * Parses an ip string into an AddressRange. 693 * Throws: 694 * ConvException if one of the numbers is not an unsigned byte 695 */ 696 public static AddressRange parse(string address) { 697 AddressRange ret; 698 string[] spl = address.split("."); 699 if(spl.length == 4) { 700 // ipv4 701 ret.addressFamily = AddressFamily.INET; 702 foreach(string s ; spl) { 703 if(s == "*") { 704 ret.ranges ~= Range(ubyte.min, ubyte.max); 705 } else if(s.indexOf("-") > 0) { 706 auto range = Range(to!ubyte(s[0..s.indexOf("-")]), to!ubyte(s[s.indexOf("-")+1..$])); 707 if(range.min > range.max) { 708 auto sw = range.max; 709 range.max = range.min; 710 range.min = sw; 711 } 712 ret.ranges ~= range; 713 } else { 714 ubyte value = to!ubyte(s); 715 ret.ranges ~= Range(value, value); 716 } 717 } 718 return ret; 719 } else { 720 // try ipv6 721 ret.addressFamily = AddressFamily.INET6; 722 spl = address.split("::"); 723 if(spl.length) { 724 string[] a = spl[0].split(":"); 725 string[] b = (spl.length > 1 ? spl[1] : "").split(":"); 726 if(a.length + b.length <= 8) { 727 while(a.length + b.length != 8) { 728 a ~= "0"; 729 } 730 foreach(s ; a ~ b) { 731 if(s == "*") { 732 ret.ranges ~= Range(ushort.min, ushort.max); 733 } else if(s.indexOf("-") > 0) { 734 auto range = Range(s[0..s.indexOf("-")].to!ushort(16), s[s.indexOf("-")+1..$].to!ushort(16)); 735 if(range.min > range.max) { 736 auto sw = range.max; 737 range.max = range.min; 738 range.min = sw; 739 } 740 } else { 741 ushort num = s.to!ushort(16); 742 ret.ranges ~= Range(num, num); 743 } 744 } 745 } 746 } 747 } 748 return ret; 749 } 750 751 public AddressFamily addressFamily; 752 753 private Range[] ranges; 754 755 /** 756 * Checks if the given address is in this range. 757 * Params: 758 * address = an address of ip version 4 or 6 759 * Returns: true if it's in the range, false otherwise 760 * Example: 761 * --- 762 * auto range = AddressRange.parse("192.168.0-64.*"); 763 * assert(range.contains(new InternetAddress("192.168.0.1"), 0)); 764 * assert(range.contains(new InternetAddress("192.168.64.255"), 0)); 765 * assert(range.contains(new InternetAddress("192.168.255.255"), 0)); 766 * --- 767 */ 768 public bool contains(Address address) { 769 size_t[] bytes; 770 if(cast(InternetAddress)address) { 771 if(this.addressFamily != addressFamily.INET) return false; 772 InternetAddress v4 = cast(InternetAddress)address; 773 bytes = [(v4.addr >> 24) & 255, (v4.addr >> 16) & 255, (v4.addr >> 8) & 255, v4.addr & 255]; 774 } else if(cast(Internet6Address)address) { 775 if(this.addressFamily != AddressFamily.INET6) return false; 776 ubyte last; 777 foreach(i, ubyte b; (cast(Internet6Address)address).addr) { 778 if(i % 2 == 0) { 779 last = b; 780 } else { 781 bytes ~= last << 8 | b; 782 } 783 } 784 } 785 if(bytes.length == this.ranges.length) { 786 foreach(size_t i, Range range; this.ranges) { 787 if(bytes[i] < range.min || bytes[i] > range.max) return false; 788 } 789 return true; 790 } else { 791 return false; 792 } 793 } 794 795 /** 796 * Converts this range into a string. 797 * Returns: the address range formatted into a string 798 * Example: 799 * --- 800 * assert(AddressRange.parse("*.0-255.79-1.4-4").toString() == "*.*.1-79.4"); 801 * --- 802 */ 803 public string toString() { 804 string pre, suf; 805 string[] ret; 806 size_t max = this.addressFamily == AddressFamily.INET ? ubyte.max : ushort.max; 807 bool hex = this.addressFamily == AddressFamily.INET6; 808 Range[] ranges = this.ranges; 809 if(hex) { 810 if(ranges[0].is0) { 811 pre = "::"; 812 while(ranges.length && ranges[0].is0) { 813 ranges = ranges[1..$]; 814 } 815 } else if(ranges[$-1].is0) { 816 suf = "::"; 817 while(ranges.length && ranges[$-1].is0) { 818 ranges = ranges[0..$-1]; 819 } 820 } else { 821 //TODO zeroes in the centre 822 } 823 } 824 foreach(Range range ; ranges) { 825 ret ~= range.toString(max, hex); 826 } 827 return pre ~ ret.join(hex ? ":" : ".") ~ suf; 828 } 829 830 private static struct Range { 831 832 size_t min, max; 833 834 public pure nothrow @property @safe @nogc bool is0() { 835 return this.min == 0 && this.max == 0; 836 } 837 838 public string toString(size_t max, bool hex) { 839 string conv(size_t num) { 840 if(hex) return to!string(num, 16).toLower; 841 else return to!string(num); 842 } 843 if(this.min == 0 && this.max >= max) { 844 return "*"; 845 } else if(this.min != this.max) { 846 return conv(this.min) ~ "-" ~ conv(this.max); 847 } else { 848 return conv(this.min); 849 } 850 } 851 852 } 853 854 }