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 }