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/handler/rcon.d, selery/hub/handler/rcon.d)
28  */
29 module selery.hub.handler.rcon;
30 
31 import core.atomic : atomicOp;
32 import core.thread : Thread;
33 
34 import std.bitmanip : write, peek, nativeToLittleEndian;
35 import std.concurrency : spawn;
36 import std.conv : to;
37 import std.datetime : dur;
38 import std.socket;
39 import std.system : Endian;
40 
41 import sel.hncom.status : RemoteCommand;
42 import sel.server.query : Query;
43 import sel.server.util;
44 
45 import selery.about;
46 import selery.hub.server : HubServer;
47 import selery.util.thread : SafeThread;
48 
49 /**
50  * The handler thread only accepts connections on a blocking
51  * TCP socket and starts new sessions in another thread, if
52  * the address of the client is not blocked by the server.
53  */
54 final class RconHandler : GenericServer {
55 
56 	private shared HubServer server;
57 	
58 	public shared this(shared HubServer server) {
59 		super(server.info);
60 		this.server = server;
61 	}
62 
63 	protected override shared void startImpl(Address address, shared Query query) {
64 		Socket socket = new TcpSocket(address.addressFamily);
65 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
66 		socket.blocking = true;
67 		socket.bind(address);
68 		socket.listen(8);
69 		spawn(&this.acceptClients, cast(shared)socket);
70 	}
71 
72 	private shared void acceptClients(shared Socket _socket) {
73 		debug Thread.getThis().name = "rcon_server@" ~ (cast()_socket).localAddress.toString();
74 		Socket socket = cast()_socket;
75 		while(true) {
76 			Socket client = socket.accept();
77 			new SafeThread(this.server.lang, {
78 				shared RconClient session = new shared RconClient(this.server, client);
79 				delete session;
80 			}).start();
81 		}
82 	}
83 
84 	public override shared pure nothrow @property @safe @nogc ushort defaultPort() {
85 		return ushort(25575);
86 	}
87 
88 }
89 
90 /**
91  * An Rcon session runs in a dedicated thread and only waits
92  * for data on a blocking socket after the login.
93  * 
94  * The first login packet must arrive within 1 second since
95  * the creation of the session and it must be a login packet
96  * with the password in it.
97  * If the packet doesn't arrive, its format is wrong or the 
98  * password is wrong the session is closed.
99  * 
100  * Once successfully connected the socket's receive timeout
101  * is set to infinite and the session only waits for remote
102  * commands.
103  * 
104  * The session never times out and it's only closed when the
105  * remote socket has been closed or a packet with the wrong
106  * format is received.
107  */
108 final class RconClient {
109 
110 	private static shared uint _id;
111 	private static shared int commandsCount = 1;
112 
113 	public immutable uint id;
114 
115 	private shared HubServer server;
116 
117 	private shared Socket socket;
118 	private immutable string remoteAddress;
119 
120 	private shared int[int] commandTable;
121 	
122 	public shared this(shared HubServer server, Socket socket) {
123 		this.id = atomicOp!"+="(_id, 1);
124 		this.server = server;
125 		this.socket = cast(shared)socket;
126 		this.remoteAddress = socket.remoteAddress.to!string;
127 		debug Thread.getThis().name = "rcon_client#" ~ to!string(this.id) ~ "@" ~ this.remoteAddress;
128 		// wait for the login or disconnect
129 		ubyte[] payload = new ubyte[14 + server.config.hub.rconPassword.length];
130 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(1));
131 		auto recv = socket.receive(payload);
132 		if(recv >= 14) {
133 			// format is length(int32le), requestId(int32le), packetId(int32le), payload(ubyte[]), padding(x0, x0)
134 			if(payload[8] == 3 && payload[12..$-2] == server.config.hub.rconPassword) {
135 				this.send(payload[4..8], 2);
136 				server.add(this);
137 				this.loop();
138 				server.remove(this);
139 			} else {
140 				// wrong password or packet
141 				this.send(payload[4..8], -1);
142 			}
143 		}
144 		socket.close();
145 	}
146 
147 	/**
148 	 * Waits for a command packet and let the server parse
149 	 * it if it is longer than 0 bytes.
150 	 */
151 	protected shared void loop() {
152 		Socket socket = cast()this.socket;
153 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"msecs"(0)); // it was changed because the first packet couldn't be sent instantly
154 		// the session doesn't timeout
155 		// socket.blocking = true;
156 		ubyte[] buffer = new ubyte[1446];
157 		while(true) {
158 			auto recv = socket.receive(buffer);
159 			if(recv < 14 || buffer[8] != 2) return; // connection closed, invalid packet format or invalid packet id
160 			if(recv >= 15 && peek!(int, Endian.littleEndian)(buffer, 8) == 2) {
161 				// only handle commands that are at least 1-character long
162 				this.commandTable[commandsCount] = peek!(int, Endian.littleEndian)(buffer, 4);
163 				this.server.handleCommand(cast(string)buffer[12..recv-2], RemoteCommand.RCON, socket.remoteAddress, commandsCount);
164 				atomicOp!"+="(commandsCount, 1);
165 			}
166 		}
167 	}
168 
169 	/**
170 	 * Sends a packet back using the given request id.
171 	 */
172 	public shared ptrdiff_t send(ubyte[4] request_id, int id, ubyte[] payload=[]) {
173 		return this.send(request_id ~ nativeToLittleEndian(id) ~ payload ~ cast(ubyte[])[0, 0]);
174 	}
175 	
176 	public shared ptrdiff_t send(const(void)[] data) {
177 		data = nativeToLittleEndian(data.length.to!uint) ~ data;
178 		return (cast()this.socket).send(data);
179 	}
180 
181 	public shared void consoleMessage(string message, int id) {
182 		auto ptr = id in this.commandTable;
183 		if(ptr) {
184 			this.send(nativeToLittleEndian(*ptr), 0, cast(ubyte[])message);
185 			this.commandTable.remove(id);
186 		}
187 	}
188 
189 	/**
190 	 * Represented as "Rcon(id, address:port)".
191 	 * Example:
192 	 * ---
193 	 * log(rcon);
194 	 * // Rcon(54, [::1]:54123)
195 	 * ---
196 	 */
197 	public shared inout string toString() {
198 		return "Rcon(" ~ to!string(this.id) ~ ", " ~ this.remoteAddress ~ ")";
199 	}
200 	
201 }