1 /* 2 * Copyright (c) 2017-2019 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: Copyright (c) 2017-2019 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 imageformats : ImageIOException, read_png_header_from_mem; 56 57 import myip : privateAddresses, publicAddress4; 58 59 import sel.format : Format; 60 import sel.server.client : Client; 61 import sel.server.query : Query; 62 import sel.server.util : ServerInfo, PlayerHandler = Handler; 63 64 import selery.about; 65 import selery.config : Config; 66 import selery.event.event : EventListener; 67 import selery.event.hub : HubServerEvent, LogEvent; 68 import selery.hncom.login : HubInfo, NodeInfo; 69 import selery.hncom.status : Log; 70 import selery.hub.handler : Handler; 71 import selery.hub.hncom : AbstractNode; 72 import selery.hub.player : PlayerSession; 73 import selery.hub.plugin.plugin : HubPluginInfo; 74 import selery.lang : Translation; 75 import selery.log : Message, Logger; 76 import selery.plugin : Plugin; 77 import selery.server : Server; 78 import selery.util.thread; 79 import selery.util.util : milliseconds; 80 81 import terminal : Terminal; 82 83 struct Icon { 84 85 string url; 86 87 ubyte[] data; 88 string base64data; 89 90 static Icon fromData(void[] _data) { 91 ubyte[] data = cast(ubyte[])_data; 92 return Icon("", data, "data:image/png;base64," ~ Base64.encode(data).idup); 93 } 94 95 static Icon fromURL(string url, void[] data) { 96 auto ret = fromData(data); 97 ret.url = url; 98 return ret; 99 } 100 101 } 102 103 class HubServer : /*EventListener!HubServerEvent, */PlayerHandler, Server { 104 105 public immutable bool lite; 106 107 public immutable ulong id; 108 private shared ulong uuid_count; 109 110 private immutable ulong started; 111 112 public EventListener!HubServerEvent eventListener; 113 114 private shared Config _config; 115 private shared ServerLogger _logger; 116 private shared const(AddressRange)[] _accepted_nodes; 117 private shared Icon _icon; 118 private shared ServerInfo _info; 119 private shared Query _query; 120 121 private shared Plugin[] _plugins; 122 123 private shared uint n_max = 0; //TODO replace with _info.max 124 125 private shared uint n_upload, n_download; 126 127 private shared Handler handler; 128 129 private shared AbstractNode[uint] nodes; 130 private shared AbstractNode[] main_nodes; 131 private shared AbstractNode[string] nodesNames; 132 private shared size_t[string] n_plugins; 133 134 private shared PlayerSession[uint] _players; 135 136 public shared this(bool lite, Config config, Plugin[] plugins=[], string[] args=[]) { 137 138 assert(config.files !is null); 139 assert(config.lang !is null); 140 assert(config.hub !is null); 141 142 debug Thread.getThis().name = "hub_server"; 143 144 this.lite = lite; 145 146 this.eventListener = new EventListener!HubServerEvent(); 147 148 this._info = new shared ServerInfo(); 149 if(config.hub.query) { 150 this._query = new shared Query(this._info); 151 this._query.software = Software.name ~ " " ~ Software.displayVersion; 152 } 153 154 AddressRange[] acceptedNodes; 155 foreach(node ; config.hub.acceptedNodes) { 156 acceptedNodes ~= AddressRange.parse(node); 157 } 158 this._accepted_nodes = cast(shared const)acceptedNodes; 159 160 Terminal terminal = new Terminal(); 161 162 terminal.title = config.hub.displayName ~ " | " ~ (!lite ? "hub | " : "") ~ Software.simpleDisplay; 163 164 Message[][] errors = this.load(config); 165 166 this._logger = cast(shared)new ServerLogger(this, terminal); 167 168 this.logger.log(Translation("startup.starting", [Format.green ~ Software.name ~ Format.reset ~ " " ~ Format.white ~ Software.fullVersion ~ Format.reset ~ " " ~ Software.fullCodename])); 169 170 static if(!__supported) { 171 this.logger.logWarning(Translation("startup.unsupported", [Software.name])); 172 } 173 174 // print error message from config loading 175 foreach(message ; errors) { 176 this.logger.logMessage(message); 177 } 178 179 this.id = uniform!"[]"(ulong.min, ulong.max); 180 this.uuid_count = uniform!"[]"(ulong.min, ulong.max); 181 182 auto pr = privateAddresses; 183 if(pr.length) this.logger.log(Translation("startup.privateAddresses", pr.join(", "))); 184 immutable pu4 = publicAddress4; 185 if(pu4.length) this.logger.log(Translation("startup.publicAddress", pu4)); 186 187 this.handler = new shared Handler(this, this._info, this._query); 188 189 this._plugins = cast(shared Plugin[])plugins; 190 191 // load plugins 192 foreach(_plugin ; _plugins) { 193 auto plugin = cast(HubPluginInfo)_plugin; 194 plugin.load(this); 195 if(plugin.main) { 196 auto a = [ 197 Format.green ~ plugin.name ~ Format.reset, 198 Format.white ~ (plugin.authors.length ? plugin.authors.join(Format.reset ~ ", " ~ Format.white) : "?") ~ Format.reset, 199 Format.white ~ plugin.version_[1..$] 200 ]; 201 this.logger.log(Translation("startup.plugin.enabled" ~ (plugin.version_.startsWith("v") ? ".version" : (plugin.authors.length ? ".author" : "")), a)); 202 } 203 } 204 205 //TODO load plugins' language files 206 207 // call @start 208 foreach(plugin ; _plugins) { 209 foreach(del ; plugin.onstart) del(); 210 } 211 212 this.started = milliseconds; 213 214 if(!this.lite) this.logger.log(Translation("startup.started")); 215 216 int last_online, last_max = this.maxPlayers; 217 size_t next_analytics = 0; 218 while(true) { 219 uint online = this.onlinePlayers.to!uint; 220 if(online != last_online || this.maxPlayers != last_max) { 221 last_online = online; 222 last_max = this.maxPlayers; 223 foreach(node ; this.nodes) { 224 node.updatePlayers(last_online, last_max); 225 } 226 } 227 Thread.sleep(dur!"msecs"(1000)); 228 } 229 230 } 231 232 /** 233 * Loads the configuration file. 234 * - validates the motds 235 * - validates protocols 236 * - loads and validate favicon 237 * - validate accepted language(s) 238 * - load languages 239 */ 240 private shared Message[][] load(ref Config config) { 241 Message[][] errors; 242 // MOTDs and protocols 243 this._info.motd.raw = config.hub.displayName; 244 if(config.hub.bedrock) with(config.hub.bedrock) { 245 motd = motd.replaceAll(ctRegex!"&([0-9a-zk-or])", "§$1"); 246 motd = motd.replace(";", ""); 247 motd ~= Format.reset; 248 this._info.motd.bedrock = motd; 249 validateProtocols(protocols, supportedBedrockProtocols, supportedBedrockProtocols); 250 } 251 if(config.hub.java) with(config.hub.java) { 252 motd = motd.replaceAll(ctRegex!"&([0-9a-zk-or])", "§$1"); 253 motd = motd.replace("\\n", "\n"); 254 this._info.motd.java = motd; 255 validateProtocols(protocols, supportedJavaProtocols, supportedJavaProtocols); 256 } 257 // icon 258 Icon icon; 259 if(exists(config.hub.favicon) && isFile(config.hub.favicon)) { 260 icon = Icon.fromData(read(config.hub.favicon)); 261 } else if(config.hub.favicon.startsWith("http://") || config.hub.favicon.startsWith("https://")) { 262 immutable cached = "icon_" ~ Base64.encode(cast(ubyte[])config.hub.favicon).idup; 263 if(!config.files.hasTemp(cached)) { 264 try { 265 static import std.net.curl; 266 std.net.curl.download(config.hub.favicon, config.files.temp ~ cached); 267 } catch(CurlException e) { 268 errors ~= Message.convert(Format.yellow, Translation("warning.iconFailed", config.hub.favicon, e.msg)); 269 } 270 } 271 if(config.files.hasTemp(cached)) { 272 icon = Icon.fromURL(config.hub.favicon, config.files.readTemp(cached)); 273 } 274 } 275 if(icon.data.length) { 276 bool valid = false; 277 try { 278 auto header = read_png_header_from_mem(icon.data); 279 if(header.width == 64 && header.height == 64) valid = true; 280 } catch(ImageIOException) {} 281 if(!valid) { 282 errors ~= Message.convert(Format.yellow, Translation("warning.invalidIcon", config.hub.favicon)); 283 icon = Icon.init; 284 } 285 } 286 this._icon = cast(shared)icon; 287 this._info.favicon = this._icon.base64data; 288 // save new config 289 this._config = cast(shared)config; 290 return errors; 291 } 292 293 public shared void shutdown() { 294 this.handler.shutdown(); 295 foreach(node ; this.nodes) node.onClosed(false); 296 import core.stdc.stdlib : exit; 297 this.logger.log("Shutting down"); 298 exit(0); 299 } 300 301 public shared nothrow @property UUID nextUUID() { 302 ubyte[16] data = nativeToBigEndian(this.id) ~ nativeToBigEndian(this.uuid_count); 303 atomicOp!"+="(this.uuid_count, 1); 304 return UUID(data); 305 } 306 307 public shared nothrow @property @trusted @nogc ulong nextPool() { 308 ulong pool = this.uuid_count; 309 atomicOp!"+="(this.uuid_count, uint.max); 310 return pool; 311 } 312 313 /** 314 * Gets the server's uptime in milliseconds. 315 */ 316 public shared @property @safe const uint uptime() { 317 return cast(uint)(milliseconds - this.started); 318 } 319 320 /** 321 * Gets the server's configuration. 322 */ 323 public override shared nothrow @property @trusted @nogc const(Config) config() { 324 return cast()this._config; 325 } 326 327 public override shared @property Logger logger() { 328 return cast()this._logger; 329 } 330 331 public override shared pure nothrow @property @trusted @nogc const(Plugin)[] plugins() { 332 return cast(const(Plugin)[])this._plugins; 333 } 334 335 public final shared nothrow @property @safe @nogc shared(ServerInfo) info() { 336 return this._info; 337 } 338 339 public shared nothrow @property @trusted @nogc const(Icon) icon() { 340 return cast()this._icon; 341 } 342 343 /// ditto 344 public shared nothrow @property @safe @nogc const uint upload() { 345 return this.n_upload; 346 } 347 348 /// ditto 349 public shared nothrow @property @safe @nogc const uint download() { 350 return this.n_download; 351 } 352 353 /** 354 * Gets the number of online players. 355 */ 356 public shared nothrow @property @safe @nogc const uint onlinePlayers() { 357 version(X86_64) { 358 return cast(uint)this._players.length; 359 } else { 360 return this._players.length; 361 } 362 } 363 364 /** 365 * Gets the number of max players. 366 */ 367 public shared nothrow @property @safe @nogc const int maxPlayers() { 368 return this.n_max; 369 } 370 371 public shared @property @safe @nogc void updateMaxPlayers() { 372 int max = 0; 373 foreach(node ; this.nodes) { 374 if(node.max == NodeInfo.UNLIMITED) { 375 this.n_max = HubInfo.UNLIMITED; 376 return; 377 } else { 378 max += node.max; 379 } 380 } 381 this.n_max = max; 382 } 383 384 /** 385 * Indicates whether the server is full. 386 */ 387 public shared @property @safe @nogc const(bool) full() { 388 if(this.maxPlayers == HubInfo.UNLIMITED) return false; 389 foreach(node ; this.nodes) { 390 if(!node.full) return false; 391 } 392 return true; 393 } 394 395 /** 396 * Gets the online players. 397 */ 398 public shared @property shared(PlayerSession[]) players() { 399 return this._players.values; 400 } 401 402 /** 403 * Handles a command. 404 */ 405 public shared void handleCommand(string command, ubyte origin, Address sender, int commandId) { 406 shared AbstractNode recv; 407 if(this.lite) { 408 recv = this.nodes.values[0]; 409 } else { 410 string name = ""; 411 immutable space = command.indexOf(" "); 412 if(space != -1) { 413 name = command[0..space]; 414 command = command[space..$].strip; 415 if(command.length == 0) return; 416 } 417 recv = this.nodeByName(name); 418 if(recv is null) return; //TODO print error message 419 } 420 recv.remoteCommand(command, origin, sender, commandId); 421 } 422 423 /** 424 * Handles a log. 425 */ 426 public shared void handleLog(string node, Log.Message[] messages, ulong timestamp, int commandId, int worldId, string worldName) { 427 Message[] log; 428 if(node.length) log ~= Message("[node/" ~ node ~ "]"); 429 if(worldName.length) log ~= Message("[world/" ~ worldName ~ "]"); 430 if(log.length) log ~= Message(" "); 431 // convert from Log.Message[] to Message[] 432 foreach(message ; messages) { 433 if(message.translation) log ~= Message(Translation(message.message, message.params)); 434 else log ~= Message(message.message); 435 } 436 (cast()this._logger).logWith(log, commandId, worldId); 437 } 438 439 public shared bool acceptNode(Address address) { 440 if(this.config.hub.maxNodes != 0) { 441 if(this.nodes.length >= this.config.hub.maxNodes) return false; 442 } 443 // check if it's an IPv4-mapped in IPv6 444 if(cast(Internet6Address)address) { 445 auto v6 = cast(Internet6Address)address; 446 ubyte[16] bytes = v6.addr; 447 if(bytes[10] == 255 && bytes[11] == 255) { // ::ffff:127.0.0.1 448 address = new InternetAddress(to!string(bytes[12]) ~ "." ~ to!string(bytes[13]) ~ "." ~ to!string(bytes[14]) ~ "." ~ to!string(bytes[15]), v6.port); 449 } 450 } 451 foreach(ar ; this._accepted_nodes) { 452 if((cast()ar).contains(address)) return true; 453 } 454 return false; 455 } 456 457 public shared nothrow @property @safe @nogc bool hasNodes() { 458 return this.nodes.length != 0; 459 } 460 461 /** 462 * Returns: the first main node which is not full 463 */ 464 public shared nothrow @property @safe @nogc shared(AbstractNode) mainNode() { 465 foreach(node ; this.main_nodes) { 466 if(node.main && (node.max == NodeInfo.UNLIMITED || node.online < node.max)) return node; 467 } 468 return null; 469 } 470 471 public shared nothrow @property @safe shared(AbstractNode)[] mainNodes() { 472 shared AbstractNode[] nodes; 473 foreach(node ; this.main_nodes) { 474 if(node.main && (node.max == NodeInfo.UNLIMITED || node.online < node.max)) nodes ~= node; 475 } 476 return nodes; 477 } 478 479 public shared nothrow shared(AbstractNode) nodeByName(string name) { 480 auto ptr = name in this.nodesNames; 481 return ptr ? *ptr : null; 482 } 483 484 public shared nothrow shared(AbstractNode) nodeById(uint id) { 485 auto ptr = id in this.nodes; 486 return ptr ? *ptr : null; 487 } 488 489 public shared @property string[] nodeNames() { 490 return this.nodesNames.keys; 491 } 492 493 public shared @property shared(AbstractNode[]) nodesList() { 494 return this.nodes.values; 495 } 496 497 public synchronized shared void add(shared AbstractNode node) { 498 if(!this.lite) this.logger.log(Format.green, "+ ", Format.reset, node.toString()); 499 this.nodes[node.id] = node; 500 this.nodesNames[node.name] = node; 501 // update players 502 this.updateMaxPlayers(); 503 // add to main, if main 504 if(node.main) this.main_nodes ~= node; 505 // add plugins 506 foreach(plugin ; node.plugins) { 507 string str = plugin.name ~ " " ~ plugin.version_; 508 if(str in this.n_plugins) { 509 atomicOp!"+="(this.n_plugins[str], 1); 510 } else { 511 this.n_plugins[str] = 1; 512 //TODO add to _query.plugins 513 } 514 } 515 // notify other nodes 516 foreach(shared AbstractNode on ; this.nodes) { 517 on.addNode(node); 518 } 519 } 520 521 public synchronized shared void remove(shared AbstractNode node) { 522 this.logger.log(Format.red, "- ", Format.reset, node.toString()); 523 this.nodes.remove(node.id); 524 this.nodesNames.remove(node.name); 525 // update players 526 this.updateMaxPlayers(); 527 // remove from main, if main 528 if(node.main) { 529 foreach(i, n; this.main_nodes) { 530 if(n.id == node.id) { 531 this.main_nodes = this.main_nodes[0..i] ~ this.main_nodes[i+1..$]; 532 break; 533 } 534 } 535 } 536 // remove plugins 537 foreach(plugin ; node.plugins) { 538 string str = plugin.name ~ " " ~ plugin.version_; 539 auto ptr = str in this.n_plugins; 540 if(ptr) { 541 atomicOp!"-="(*ptr, 1); 542 if(*ptr == 0) { 543 this.n_plugins.remove(str); 544 //TODO remove from _query.plugins 545 } 546 } 547 } 548 // notify other nodes 549 foreach(shared AbstractNode on ; this.nodes) { 550 on.removeNode(node); 551 } 552 } 553 554 public override shared void onClientJoin(shared Client client) { 555 auto player = new shared PlayerSession(this, client); 556 if(player.firstConnect()) this._players[player.id] = player; 557 } 558 559 public override shared void onClientLeft(shared Client client) { 560 auto player = client.id in this._players; 561 if(player) { 562 this._players.remove(client.id); 563 (*player).onClosed(); // remove from the node 564 } 565 } 566 567 public override shared void onClientPacket(shared Client client, ubyte[] packet) { 568 auto player = client.id in this._players; 569 if(player) { 570 (*player).sendToNode(packet); 571 } 572 } 573 574 public shared void onBedrockClientRequestChunkRadius(shared Client client, uint viewDistance) { 575 //TODO select player and update if changed (the node will send the confirmation back) 576 } 577 578 public shared void onJavaClientClientSettings(shared Client client, string language, ubyte viewDistance, uint chatMode, bool chatColors, ubyte skinParts, uint mainHand) { 579 //TODO select player and update if changed 580 } 581 582 public shared nothrow shared(PlayerSession) playerFromId(immutable(uint) id) { 583 auto ptr = id in this._players; 584 return ptr ? *ptr : null; 585 } 586 587 public shared shared(PlayerSession) playerFromIdentifier(ubyte[] idf) { 588 foreach(shared PlayerSession player ; this.players) { 589 if(player.iusername == idf) return player; 590 } 591 return null; 592 } 593 594 } 595 596 private class ServerLogger : Logger { 597 598 private shared HubServer server; 599 600 public this(shared HubServer server, Terminal terminal) { 601 super(terminal, server.lang); 602 this.server = server; 603 } 604 605 protected override void logImpl(Message[] messages) { 606 this.logWith(messages, Log.NO_COMMAND, Log.NO_WORLD); 607 } 608 609 public void logWith(Message[] messages, int commandId, int worldId) { 610 (cast()this.server).eventListener.callEventIfExists!LogEvent(this.server, messages, commandId, worldId); 611 super.logImpl(messages); 612 } 613 614 } 615 616 /** 617 * Stores a range of ip addresses. 618 */ 619 struct AddressRange { 620 621 /** 622 * Parses an ip string into an AddressRange. 623 * Throws: 624 * ConvException if one of the numbers is not an unsigned byte 625 */ 626 public static AddressRange parse(string address) { 627 AddressRange ret; 628 string[] spl = address.split("."); 629 if(spl.length == 4) { 630 // ipv4 631 ret.addressFamily = AddressFamily.INET; 632 foreach(string s ; spl) { 633 if(s == "*") { 634 ret.ranges ~= Range(ubyte.min, ubyte.max); 635 } else if(s.indexOf("-") > 0) { 636 auto range = Range(to!ubyte(s[0..s.indexOf("-")]), to!ubyte(s[s.indexOf("-")+1..$])); 637 if(range.min > range.max) { 638 auto sw = range.max; 639 range.max = range.min; 640 range.min = sw; 641 } 642 ret.ranges ~= range; 643 } else { 644 ubyte value = to!ubyte(s); 645 ret.ranges ~= Range(value, value); 646 } 647 } 648 return ret; 649 } else { 650 // try ipv6 651 ret.addressFamily = AddressFamily.INET6; 652 spl = address.split("::"); 653 if(spl.length) { 654 string[] a = spl[0].split(":"); 655 string[] b = (spl.length > 1 ? spl[1] : "").split(":"); 656 if(a.length + b.length <= 8) { 657 while(a.length + b.length != 8) { 658 a ~= "0"; 659 } 660 foreach(s ; a ~ b) { 661 if(s == "*") { 662 ret.ranges ~= Range(ushort.min, ushort.max); 663 } else if(s.indexOf("-") > 0) { 664 auto range = Range(s[0..s.indexOf("-")].to!ushort(16), s[s.indexOf("-")+1..$].to!ushort(16)); 665 if(range.min > range.max) { 666 auto sw = range.max; 667 range.max = range.min; 668 range.min = sw; 669 } 670 } else { 671 ushort num = s.to!ushort(16); 672 ret.ranges ~= Range(num, num); 673 } 674 } 675 } 676 } 677 } 678 return ret; 679 } 680 681 public AddressFamily addressFamily; 682 683 private Range[] ranges; 684 685 /** 686 * Checks if the given address is in this range. 687 * Params: 688 * address = an address of ip version 4 or 6 689 * Returns: true if it's in the range, false otherwise 690 * Example: 691 * --- 692 * auto range = AddressRange.parse("192.168.0-64.*"); 693 * assert(range.contains(new InternetAddress("192.168.0.1"), 0)); 694 * assert(range.contains(new InternetAddress("192.168.64.255"), 0)); 695 * assert(range.contains(new InternetAddress("192.168.255.255"), 0)); 696 * --- 697 */ 698 public bool contains(Address address) { 699 size_t[] bytes; 700 if(cast(InternetAddress)address) { 701 if(this.addressFamily != addressFamily.INET) return false; 702 InternetAddress v4 = cast(InternetAddress)address; 703 bytes = [(v4.addr >> 24) & 255, (v4.addr >> 16) & 255, (v4.addr >> 8) & 255, v4.addr & 255]; 704 } else if(cast(Internet6Address)address) { 705 if(this.addressFamily != AddressFamily.INET6) return false; 706 ubyte last; 707 foreach(i, ubyte b; (cast(Internet6Address)address).addr) { 708 if(i % 2 == 0) { 709 last = b; 710 } else { 711 bytes ~= last << 8 | b; 712 } 713 } 714 } 715 if(bytes.length == this.ranges.length) { 716 foreach(size_t i, Range range; this.ranges) { 717 if(bytes[i] < range.min || bytes[i] > range.max) return false; 718 } 719 return true; 720 } else { 721 return false; 722 } 723 } 724 725 /** 726 * Converts this range into a string. 727 * Returns: the address range formatted into a string 728 * Example: 729 * --- 730 * assert(AddressRange.parse("*.0-255.79-1.4-4").toString() == "*.*.1-79.4"); 731 * --- 732 */ 733 public string toString() { 734 string pre, suf; 735 string[] ret; 736 size_t max = this.addressFamily == AddressFamily.INET ? ubyte.max : ushort.max; 737 bool hex = this.addressFamily == AddressFamily.INET6; 738 Range[] ranges = this.ranges; 739 if(hex) { 740 if(ranges[0].is0) { 741 pre = "::"; 742 while(ranges.length && ranges[0].is0) { 743 ranges = ranges[1..$]; 744 } 745 } else if(ranges[$-1].is0) { 746 suf = "::"; 747 while(ranges.length && ranges[$-1].is0) { 748 ranges = ranges[0..$-1]; 749 } 750 } else { 751 //TODO zeroes in the centre 752 } 753 } 754 foreach(Range range ; ranges) { 755 ret ~= range.toString(max, hex); 756 } 757 return pre ~ ret.join(hex ? ":" : ".") ~ suf; 758 } 759 760 private static struct Range { 761 762 size_t min, max; 763 764 public pure nothrow @property @safe @nogc bool is0() { 765 return this.min == 0 && this.max == 0; 766 } 767 768 public string toString(size_t max, bool hex) { 769 string conv(size_t num) { 770 if(hex) return to!string(num, 16).toLower; 771 else return to!string(num); 772 } 773 if(this.min == 0 && this.max >= max) { 774 return "*"; 775 } else if(this.min != this.max) { 776 return conv(this.min) ~ "-" ~ conv(this.max); 777 } else { 778 return conv(this.min); 779 } 780 } 781 782 } 783 784 }