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 }