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 }