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 }