1 module vibe.rcon.client; 2 3 import std.regex; 4 import std.array; 5 import std.string; 6 import std.exception; 7 8 import vibe.core.net; 9 10 import vibe.rcon.protocol; 11 12 final class RCONClient { 13 @safe: 14 15 private { 16 int rollingId = 0; 17 TCPConnection connection; 18 } 19 20 this(string host, ushort port) { 21 this(connectTCP(host, port)); 22 } 23 24 this(TCPConnection connection) { 25 this.connection = connection; 26 } 27 28 RCONPacket receive() { 29 return RCONPacket.read(connection); 30 } 31 32 int send(string message) { 33 return send(RCONPacket.Type.EXEC_COMMAND, message); 34 } 35 36 int send(RCONPacket.Type type, string message) { 37 auto id = nextId; 38 RCONPacket(id, type, message).write(connection); 39 return id; 40 } 41 42 void send(RCONPacket packet) { 43 packet.write(connection); 44 } 45 46 bool authenticate(string password) { 47 auto authID = send(RCONPacket.Type.AUTH, password); 48 49 // Source always sends an empty response before acknowledging success/failure 50 auto empty = receive(); 51 enforce(empty.type == RCONPacket.Type.RESPONSE_VALUE); 52 enforce(empty.message == ""); 53 54 auto authResponse = receive(); 55 enforce(authResponse.type == RCONPacket.Type.AUTH_RESPONSE); 56 57 return authResponse.id == authID; 58 } 59 60 string exec(string command) { 61 // This implements a trick that works for any command for handling multi-packet responses. 62 // First send the command packet, then a response packet. 63 // SRCDS responds with 0x000100 to the 2nd packet, after the first is complete. 64 auto commandId = send(RCONPacket.Type.EXEC_COMMAND, command); 65 auto endPacketId = send(RCONPacket.Type.RESPONSE_VALUE, null); 66 67 auto result = appender!string; 68 69 while (true) { 70 auto packet = receive(); 71 72 if (packet.id == commandId) { 73 enforce(packet.type == RCONPacket.Type.RESPONSE_VALUE); 74 75 result ~= packet.message; 76 } else if (packet.id == endPacketId && packet.type == RCONPacket.Type.RESPONSE_VALUE) { 77 if (packet.message == "\x00\x01\x00") break; 78 79 enforce(packet.message == ""); 80 } else { 81 throw new Exception(""); 82 } 83 } 84 85 return result.data; 86 } 87 88 string readConVar(string convar) { 89 // Console variables are responded to with something like the following: 90 // "sv_password" = "xOKK5DKotcM6YSY6" ( def. "" ) 91 // notify 92 // - Server password for entry into multiplayer games 93 // 94 // We can parse the actual value out of that. 95 96 auto response = exec(convar); 97 auto prefix = "\"%s\" = \"".format(convar); 98 enforce(response.startsWith(prefix), "Invalid convar format"); 99 100 response = response[prefix.length..$]; 101 response = replaceFirst(response, regex("\" \\( def\\. \".*\" \\)[\n\r].*$", "s"), ""); 102 return response; 103 } 104 105 void close() { 106 connection.close(); 107 } 108 109 private @property auto nextId() { 110 rollingId += 1; 111 return rollingId; 112 } 113 }