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 }