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/hncom.d, selery/hub/hncom.d)
28  */
29 module selery.hub.hncom;
30 
31 import core.atomic : atomicOp;
32 import core.thread : Thread;
33 
34 import std.algorithm : canFind;
35 import std.concurrency : spawn;
36 import std.conv : to;
37 import std.datetime : dur;
38 import std.json : JSONValue;
39 import std.math : round;
40 import std.regex : ctRegex, matchFirst;
41 import std.socket;
42 import std..string;
43 import std.system : Endian;
44 import std.zlib;
45 
46 import sel.net.modifiers : LengthPrefixedStream;
47 import sel.net.stream : TcpStream;
48 import sel.server.query : Query;
49 import sel.server.util;
50 
51 import selery.about;
52 import selery.hncom.about;
53 import selery.hncom.handler : Handler = HncomHandler;
54 import selery.hncom.io : HncomAddress, HncomUUID;
55 import selery.hub.player : WorldSession = World, PlayerSession, Skin;
56 import selery.hub.server : HubServer;
57 import selery.util.thread : SafeThread;
58 import selery.util.util : microseconds;
59 
60 import Login = selery.hncom.login;
61 import Status = selery.hncom.status;
62 import Player = selery.hncom.player;
63 
64 alias HncomStream = LengthPrefixedStream!(uint, Endian.littleEndian);
65 
66 class HncomHandler {
67 
68 	private shared HubServer server;
69 	
70 	private shared JSONValue* additionalJson;
71 
72 	private shared Address address;
73 	
74 	public shared this(shared HubServer server, shared JSONValue* additionalJson) {
75 		this.server = server;
76 		this.additionalJson = additionalJson;
77 	}
78 
79 	public shared void start(inout(string)[] accepted, ushort port) {
80 		bool v4, v6, public_;
81 		foreach(address ; accepted) {
82 			switch(address) {
83 				case "127.0.0.1":
84 					v4 = true;
85 					break;
86 				case "::1":
87 					v6 = true;
88 					break;
89 				default:
90 					if(address.canFind(":")) v6 = true;
91 					else v4 = true;
92 					public_ = true;
93 					break;
94 			}
95 		}
96 		Address address = getAddress(public_ ? (v4 ? "0.0.0.0" : "::") : (v4 ? "127.0.0.1" : "::1"), port)[0];
97 		Socket socket = new TcpSocket(v4 && v6 ? AddressFamily.INET | AddressFamily.INET6 : address.addressFamily);
98 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
99 		socket.setOption(SocketOptionLevel.IPV6, SocketOption.IPV6_V6ONLY, !v4 || !v6);
100 		socket.blocking = true;
101 		socket.bind(address);
102 		socket.listen(8);
103 		this.address = cast(shared)address;
104 		spawn(&this.acceptClients, cast(shared)socket);
105 	}
106 	
107 	private shared void acceptClients(shared Socket _socket) {
108 		debug Thread.getThis().name = "hncom_server@" ~ (cast()_socket).localAddress.toString();
109 		Socket socket = cast()_socket;
110 		while(true) {
111 			Socket client = socket.accept();
112 			Address address;
113 			try {
114 				address = client.remoteAddress;
115 			} catch(Exception) {
116 				continue;
117 			}
118 			if(this.server.acceptNode(address)) {
119 				new SafeThread(this.server.config.lang, {
120 					shared ClassicNode node = new shared ClassicNode(this.server, client, this.additionalJson);
121 					delete node;
122 				}).start();
123 			} else {
124 				client.close();
125 			}
126 		}
127 	}
128 
129 	public shared pure nothrow @property @safe @nogc shared(Address) localAddress() {
130 		return this.address;
131 	}
132 	
133 }
134 
135 /**
136  * Session of a node. It's executed in a dedicated thread.
137  */
138 abstract class AbstractNode : Handler!serverbound {
139 
140 	private static shared uint _id;
141 
142 	public immutable uint id;
143 	
144 	private shared HubServer server;
145 	private shared JSONValue* additionalJson;
146 
147 	protected HncomStream stream;
148 	
149 	private shared bool n_main;
150 	private shared string n_name;
151 	
152 	private shared uint[][ubyte] accepted;
153 	
154 	private shared uint n_max;
155 	public shared Login.NodeInfo.Plugin[] plugins;
156 	
157 	private shared PlayerSession[uint] players;
158 	private shared WorldSession[uint] _worlds;
159 	
160 	private uint n_latency;
161 	
162 	private shared float n_tps;
163 	private shared ulong n_ram;
164 	private shared float n_cpu;
165 	
166 	public shared this(shared HubServer server, shared JSONValue* additionalJson) {
167 		this.id = atomicOp!"+="(_id, 1);
168 		this.server = server;
169 		this.additionalJson = additionalJson;
170 	}
171 
172 	protected shared void exchageInfo(HncomStream stream) {
173 		with(cast()server.config.hub) {
174 			Login.HubInfo.GameInfo[ubyte] games;
175 			if(bedrock) games[__BEDROCK__] = Login.HubInfo.GameInfo(bedrock.motd, bedrock.protocols, bedrock.onlineMode, ushort(0));
176 			if(java) games[__JAVA__] = Login.HubInfo.GameInfo(java.motd, java.protocols, java.onlineMode, ushort(0));
177 			this.sendHubInfo(stream, new Login.HubInfo(server.id, server.nextPool, displayName, games, server.onlinePlayers, server.maxPlayers, server.config.lang.acceptedLanguages.dup, false, (cast()*this.additionalJson).toString()));
178 		}
179 		auto info = this.receiveNodeInfo(stream);
180 		this.n_max = info.max;
181 		this.accepted = cast(shared uint[][ubyte])info.acceptedGames;
182 		this.plugins = cast(shared)info.plugins;
183 		foreach(node ; server.nodesList) stream.send(node.addPacket.encode());
184 		server.add(this);
185 		this.loop(stream);
186 		server.remove(this);
187 		this.onClosed();
188 	}
189 
190 	protected abstract shared void sendHubInfo(HncomStream stream, Login.HubInfo packet);
191 
192 	protected abstract shared Login.NodeInfo receiveNodeInfo(HncomStream stream);
193 
194 	protected abstract shared void loop(HncomStream stream);
195 
196 	protected abstract void send(ubyte[] buffer);
197 
198 	protected shared void send(ubyte[] buffer) {
199 		return (cast()this).send(buffer);
200 	}
201 
202 	/**
203 	 * Gets the name of the node. The name is different for every node
204 	 * connected to hub and it should be used other nodes with
205 	 * the transfer function.
206 	 */
207 	public shared nothrow @property @safe @nogc const string name() {
208 		return this.n_name;
209 	}
210 	
211 	/**
212 	 * Indicates whether or not this is a main node.
213 	 * A main node is able to receive players without the
214 	 * use of the transfer function.
215 	 * Every hub should have at least one main node, otherwise
216 	 * every player that tries to connect will be disconnected with
217 	 * the 'end of stream' message.
218 	 */
219 	public shared nothrow @property @safe @nogc const bool main() {
220 		return this.n_main;
221 	}
222 	
223 	/**
224 	 * Gets the highest number of players that can connect to the node.
225 	 */
226 	public shared nothrow @property @safe @nogc const uint max() {
227 		return this.n_max;
228 	}
229 	
230 	/**
231 	 * Gets the number of players connected to the node.
232 	 */
233 	public shared nothrow @property @safe @nogc const uint online() {
234 		version(X86_64) {
235 			return cast(uint)this.players.length;
236 		} else {
237 			return this.players.length;
238 		}
239 	}
240 	
241 	/**
242 	 * Indicates whether the node is full.
243 	 */
244 	public shared nothrow @property @safe @nogc const bool full() {
245 		return this.max != Login.NodeInfo.UNLIMITED && this.online >= this.max;
246 	}
247 
248 	/**
249 	 * Gets the list of worlds loaded on the node.
250 	 */
251 	public shared nothrow @property shared(WorldSession)[] worlds() {
252 		return this._worlds.values;
253 	}
254 	
255 	/**
256 	 * Gets the node's latency (it may not be precise).
257 	 */
258 	public shared nothrow @property @safe @nogc const uint latency() {
259 		return this.n_latency;
260 	}
261 	
262 	/**
263 	 * Gets the node's usage, updated with the ResourcesUsage packet.
264 	 */
265 	public shared nothrow @property @safe @nogc const float tps() {
266 		return this.n_tps;
267 	}
268 	
269 	/// ditto
270 	public shared nothrow @property @safe @nogc const ulong ram() {
271 		return this.n_ram;
272 	}
273 	
274 	/// ditto
275 	public shared nothrow @property @safe @nogc const float cpu() {
276 		return this.n_cpu;
277 	}
278 	
279 	public shared nothrow @property @safe bool accepts(ubyte game, uint protocol) {
280 		auto p = game in this.accepted;
281 		return p && (*p).canFind(protocol);
282 	}
283 	
284 	public shared @property Status.AddNode addPacket() {
285 		return new Status.AddNode(this.id, this.name, this.main, cast(uint[][ubyte])this.accepted);
286 	}
287 
288 	protected override void handleStatusLatency(Status.Latency packet) {
289 		this.send(packet.encode());
290 	}
291 
292 	protected override void handleStatusLog(Status.Log packet) {
293 		string name;
294 		if(packet.worldId != -1) {
295 			auto world = packet.worldId in this._worlds;
296 			if(world) name = world.name;
297 		}
298 		this.server.handleLog((cast(shared)this).name, packet.message, packet.timestamp, packet.commandId, packet.worldId, name);
299 	}
300 
301 	protected override void handleStatusSendMessage(Status.SendMessage packet) {
302 		if(packet.addressees.length) {
303 			foreach(addressee ; packet.addressees) {
304 				auto node = this.server.nodeById(addressee);
305 				if(node !is null) node.sendMessage(this.id, false, packet.payload);
306 			}
307 		} else {
308 			foreach(node ; this.server.nodesList) {
309 				if(node.id != this.id) node.sendMessage(this.id, true, packet.payload);
310 			}
311 		}
312 	}
313 
314 	protected override void handleStatusUpdateMaxPlayers(Status.UpdateMaxPlayers packet) {
315 		this.n_max = packet.max;
316 		this.server.updateMaxPlayers();
317 	}
318 
319 	protected override void handleStatusUpdateUsage(Status.UpdateUsage packet) {
320 		this.n_ram = (cast(ulong)packet.ram) * 1024Lu;
321 		this.n_cpu = packet.cpu;
322 	}
323 
324 	protected override void handleStatusUpdateLanguageFiles(Status.UpdateLanguageFiles packet) {
325 		this.server.config.lang.add(packet.language, packet.messages);
326 	}
327 
328 	protected override void handleStatusAddWorld(Status.AddWorld packet) {
329 		//TODO notify the panel
330 		this._worlds[packet.worldId] = new shared WorldSession(packet.worldId, packet.groupId, packet.name, packet.dimension);
331 	}
332 
333 	protected override void handleStatusRemoveWorld(Status.RemoveWorld packet) {
334 		//TODO notify the panel
335 		this._worlds.remove(packet.worldId);
336 	}
337 
338 	protected override void handleStatusRemoveWorldGroup(Status.RemoveWorldGroup packet) {
339 		//TODO notify the panel
340 		foreach(world ; _worlds) {
341 			if(world.groupId == packet.groupId) this._worlds.remove(world.id);
342 		}
343 	}
344 
345 	protected override void handleStatusAddPlugin(Status.AddPlugin packet) {}
346 
347 	protected override void handleStatusRemovePlugin(Status.RemovePlugin packet) {}
348 
349 	protected override void handlePlayerKick(Player.Kick packet) {
350 		auto player = packet.hubId in this.players;
351 		if(player) {
352 			this.players.remove(packet.hubId);
353 			(*player).kick(packet.reason, packet.translation, packet.parameters);
354 		}
355 	}
356 
357 	protected override void handlePlayerTransfer(Player.Transfer packet) {
358 		auto player = packet.hubId in this.players;
359 		if(player) {
360 			this.players.remove(packet.hubId);
361 			(*player).connect(Player.Add.TRANSFERRED, packet.node, packet.message, packet.onFail);
362 		}
363 	}
364 
365 	protected override void handlePlayerUpdateDisplayName(Player.UpdateDisplayName packet) {
366 		auto player = packet.hubId in this.players;
367 		if(player) {
368 			(*player).displayName = packet.displayName;
369 		}
370 	}
371 
372 	protected override void handlePlayerUpdateWorld(Player.UpdateWorld packet) {
373 		auto player = packet.hubId in this.players;
374 		auto world = packet.worldId in this._worlds;
375 		if(player && world) {
376 			(*player).world = *world;
377 		}
378 	}
379 
380 	protected override void handlePlayerUpdatePermissionLevel(Player.UpdatePermissionLevel packet) {
381 		auto player = packet.hubId in this.players;
382 		if(player) {
383 			(*player).permissionLevel = packet.permissionLevel;
384 		}
385 	}
386 
387 	protected override void handlePlayerGamePacket(Player.GamePacket packet) {
388 		//TODO compress if needed and send
389 	}
390 
391 	protected override void handlePlayerSerializedGamePacket(Player.SerializedGamePacket packet) {
392 		auto player = packet.hubId in this.players;
393 		if(player) {
394 			(*player).sendFromNode(packet.payload);
395 		}
396 	}
397 
398 	protected override void handlePlayerOrderedGamePacket(Player.OrderedGamePacket packet) {
399 		auto player = packet.hubId in this.players;
400 		if(player) {
401 			(*player).sendOrderedFromNode(packet.order, packet.payload);
402 		}
403 	}
404 	
405 	/**
406 	 * Sends data to the node received from a player.
407 	 */
408 	public shared void sendTo(shared PlayerSession player, ubyte[] data) {
409 		this.send(new Player.GamePacket(player.id, data).encode());
410 	}
411 	
412 	/**
413 	 * Executes a remote command.
414 	 */
415 	public shared void remoteCommand(string command, ubyte origin, Address address, int commandId) {
416 		this.send(new Status.RemoteCommand(origin, address, command, commandId).encode());
417 	}
418 	
419 	/**
420 	 * Notifies the node that another node has connected
421 	 * to the hub.
422 	 */
423 	public shared void addNode(shared AbstractNode node) {
424 		this.send(node.addPacket.encode());
425 	}
426 	
427 	/**
428 	 * Notifies the node that another node has been
429 	 * disconnected from the hub.
430 	 */
431 	public shared void removeNode(shared AbstractNode node) {
432 		this.send(new Status.RemoveNode(node.id).encode());
433 	}
434 	
435 	/**
436 	 * Sends a message to the node.
437 	 */
438 	public shared void sendMessage(uint sender, bool broadcasted, ubyte[] payload) {
439 		this.send(new Status.ReceiveMessage(sender, broadcasted, payload).encode());
440 	}
441 	
442 	/**
443 	 * Sends the number of online players and maximum number of
444 	 * players to the node.
445 	 */
446 	public shared void updatePlayers(inout uint online, inout uint max) {
447 		this.send(new Status.UpdatePlayers(online, max).encode());
448 	}
449 	
450 	/**
451 	 * Adds a player to the node.
452 	 */
453 	public shared void addPlayer(shared PlayerSession player, ubyte reason, ubyte[] transferMessage) {
454 		this.players[player.id] = player;
455 		this.send(new Player.Add(player.id, reason, transferMessage, player.type, player.protocol, player.uuid, player.username, player.displayName, player.gameName, player.gameVersion, player.permissionLevel, player.dimension, player.viewDistance, player.address, Player.Add.ServerAddress(player.serverIp, player.serverPort), player.skin is null ? Player.Add.Skin.init : Player.Add.Skin(player.skin.name, player.skin.data.dup, player.skin.cape.dup, player.skin.geometryName, player.skin.geometryData.dup), player.language, cast(ubyte)player.inputMode, player.hncomAddData().toString()).encode());
456 	}
457 	
458 	/**
459 	 * Called when a player is transferred by the hub (not by the node)
460 	 * to another node.
461 	 */
462 	public shared void onPlayerTransferred(shared PlayerSession player) {
463 		this.onPlayerGone(player, Player.Remove.TRANSFERRED);
464 	}
465 	
466 	/**
467 	 * Called when a player lefts the server using the disconnect
468 	 * button or closing the socket.
469 	 */
470 	public shared void onPlayerLeft(shared PlayerSession player) {
471 		this.onPlayerGone(player, Player.Remove.LEFT);
472 	}
473 	
474 	/**
475 	 * Called when a player times out.
476 	 */
477 	public shared void onPlayerTimedOut(shared PlayerSession player) {
478 		this.onPlayerGone(player, Player.Remove.TIMED_OUT);
479 	}
480 	
481 	/**
482 	 * Called when a player is kicked (not by the node).
483 	 */
484 	public shared void onPlayerKicked(shared PlayerSession player) {
485 		this.onPlayerGone(player, Player.Remove.KICKED);
486 	}
487 	
488 	/**
489 	 * Generic function that removes a player from the
490 	 * node's list and sends a PlayerDisconnected packet to
491 	 * notify the node of the disconnection.
492 	 */
493 	protected shared void onPlayerGone(shared PlayerSession player, ubyte reason) {
494 		if(this.players.remove(player.id)) {
495 			this.send(new Player.Remove(player.id, reason).encode());
496 		}
497 	}
498 
499 	public shared void sendDisplayNameUpdate(shared PlayerSession player, string displayName) {
500 		this.send(new Player.UpdateDisplayName(player.id, displayName).encode());
501 	}
502 
503 	public shared void sendPermissionLevelUpdate(shared PlayerSession player, ubyte permissionLevel) {
504 		this.send(new Player.UpdatePermissionLevel(player.id, permissionLevel).encode());
505 	}
506 
507 	public shared void sendViewDistanceUpdate(shared PlayerSession player, uint viewDistance) {
508 		this.send(new Player.UpdateViewDistance(player.id, viewDistance).encode());
509 	}
510 
511 	public shared void sendLanguageUpdate(shared PlayerSession player, string language) {
512 		this.send(new Player.UpdateLanguage(player.id, language).encode());
513 	}
514 	
515 	/**
516 	 * Updates a player's latency (usually sent every 30 seconds).
517 	 */
518 	public shared void sendLatencyUpdate(shared PlayerSession player) {
519 		this.send(new Player.UpdateLatency(player.id, player.latency).encode());
520 	}
521 	
522 	/**
523 	 * Updates a player's packet loss (usually sent every 30 seconds).
524 	 */
525 	public shared void sendPacketLossUpdate(shared PlayerSession player) {
526 		this.send(new Player.UpdatePacketLoss(player.id, player.packetLoss).encode());
527 	}
528 	
529 	/**
530 	 * Called when the client closes the connection.
531 	 * Tries to transfer every connected player to the main node.
532 	 */
533 	public shared void onClosed(bool transfer=true) {
534 		if(transfer) {
535 			foreach(shared PlayerSession player ; this.players) {
536 				player.connect(Player.Add.FORCIBLY_TRANSFERRED);
537 			}
538 		} else {
539 			foreach(shared PlayerSession player ; this.players) {
540 				player.kick("disconnect.close", true, []);
541 			}
542 		}
543 	}
544 	
545 	public abstract shared inout string toString();
546 	
547 }
548 
549 class ClassicNode : AbstractNode {
550 
551 	private shared Socket socket;
552 	private immutable string remoteAddress;
553 
554 	public shared this(shared HubServer server, Socket socket, shared JSONValue* additionalJson) {
555 		super(server, additionalJson);
556 		this.socket = cast(shared)socket;
557 		this.remoteAddress = socket.remoteAddress.toString();
558 		debug Thread.getThis().name = "hncom_client#" ~ to!string(this.id);
559 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"msecs"(2500));
560 		socket.blocking = true;
561 		auto stream = new HncomStream(new TcpStream(socket, 4096));
562 		this.stream = cast(shared)stream;
563 		auto payload = stream.receive();
564 		if(payload.length && payload[0] == Login.ConnectionRequest.ID) {
565 			immutable password = server.config.hub.hncomPassword;
566 			auto request = Login.ConnectionRequest.fromBuffer(payload);
567 			this.n_name = request.name.idup;
568 			this.n_main = request.main;
569 			Login.ConnectionResponse response = new Login.ConnectionResponse();
570 			if(request.protocol > __PROTOCOL__) response.status = Login.ConnectionResponse.OUTDATED_HUB;
571 			else if(request.protocol < __PROTOCOL__) response.status = Login.ConnectionResponse.OUTDATED_NODE;
572 			else if(password.length && !password.length) response.status = Login.ConnectionResponse.PASSWORD_REQUIRED;
573 			else if(password.length && password != request.password) response.status = Login.ConnectionResponse.WRONG_PASSWORD;
574 			else if(!this.n_name.length || this.n_name.length > 32) response.status = Login.ConnectionResponse.INVALID_NAME_LENGTH;
575 			else if(!this.n_name.matchFirst(ctRegex!r"[^a-zA-Z0-9_+-.,!?:@#$%\/]").empty) response.status = Login.ConnectionResponse.INVALID_NAME_CHARACTERS;
576 			else if(server.nodeNames.canFind(this.n_name)) response.status = Login.ConnectionResponse.NAME_ALREADY_USED;
577 			else if(["reload", "stop"].canFind(this.n_name.toLower)) response.status = Login.ConnectionResponse.NAME_RESERVED;
578 			stream.send(response.encode());
579 			if(response.status == Login.ConnectionResponse.OK) {
580 				this.exchageInfo(stream);
581 			}
582 		}
583 		socket.close();
584 	}
585 
586 	protected override shared void sendHubInfo(HncomStream stream, Login.HubInfo packet) {
587 		stream.send(packet.encode());
588 	}
589 
590 	protected override shared Login.NodeInfo receiveNodeInfo(HncomStream stream) {
591 		stream.stream.socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"minutes"(5)); // giving it the time to load resorces and generate worlds
592 		auto payload = stream.receive();
593 		if(payload.length && payload[0] == Login.NodeInfo.ID) return Login.NodeInfo.fromBuffer(payload);
594 		else return Login.NodeInfo.init;
595 	}
596 
597 	protected override shared void loop(HncomStream stream) {
598 		auto _this = cast()this;
599 		stream.stream.socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"msecs"(0)); // blocking without timeout
600 		while(true) {
601 			auto payload = stream.receive();
602 			if(payload.length) _this.handleHncom(payload);
603 			else break; // connection closed or error
604 		}
605 	}
606 
607 	protected override void send(ubyte[] payload) {
608 		this.stream.send(payload);
609 	}
610 	
611 	public override shared inout string toString() {
612 		return "Node(" ~ to!string(this.id) ~ ", " ~ this.name ~ ", " ~ this.remoteAddress ~ ", " ~ to!string(this.n_main) ~ ")";
613 	}
614 
615 }
616 
617 class LiteNode : AbstractNode {
618 
619 	static import std.concurrency;
620 	
621 	public shared static bool ready = false;
622 	public shared static std.concurrency.Tid tid;
623 	
624 	private std.concurrency.Tid node;
625 
626 	public shared this(shared HubServer server, shared JSONValue* additionalJson) {
627 		super(server, additionalJson);
628 		tid = cast(shared)std.concurrency.thisTid;
629 		ready = true;
630 		this.node = cast(shared)std.concurrency.receiveOnly!(std.concurrency.Tid)();
631 		this.n_main = true;
632 		this.exchageInfo(null);
633 	}
634 
635 	protected override shared void sendHubInfo(HncomStream stream, Login.HubInfo packet) {
636 		std.concurrency.send(cast()this.node, cast(shared)packet);
637 	}
638 
639 	protected override shared Login.NodeInfo receiveNodeInfo(HncomStream stream) {
640 		return cast()std.concurrency.receiveOnly!(shared Login.NodeInfo)();
641 	}
642 
643 	protected override shared void loop(HncomStream stream) {
644 		auto _this = cast()this;
645 		while(true) {
646 			ubyte[] payload = std.concurrency.receiveOnly!(immutable(ubyte)[])().dup;
647 			if(payload.length) {
648 				_this.handleHncom(payload);
649 			} else {
650 				break;
651 			}
652 		}
653 	}
654 	
655 	protected override void send(ubyte[] buffer) {
656 		std.concurrency.send(this.node, buffer.idup);
657 	}
658 	
659 	protected override shared void send(ubyte[] buffer) {
660 		std.concurrency.send(cast()this.node, buffer.idup);
661 	}
662 	
663 	public override shared inout string toString() {
664 		return "LiteNode(" ~ to!string(this.id) ~ ")";
665 	}
666 	
667 }