1 module vibe.rcon.protocol;
2 
3 import std.format;
4 import std.bitmanip;
5 import std.exception;
6 
7 import vibe.core.net;
8 import vibe.core.stream;
9 
10 struct RCONPacket {
11     @safe:
12 
13     enum Type {
14         AUTH = 3,
15         AUTH_RESPONSE = 2,
16         EXEC_COMMAND = 2,
17         RESPONSE_VALUE = 0,
18     }
19 
20     int id;
21     Type type;
22     string message;
23 
24     static RCONPacket read(S)(S stream) if (isInputStream!S) {
25         ubyte[4] sizeBuffer;
26         stream.read(sizeBuffer);
27         auto size = littleEndianToNative!int(sizeBuffer);
28         enforce(size >= 10 && size <= 4096, "Invalid packet size");
29 
30         auto buffer = new ubyte[size];
31         stream.read(buffer);
32         return RCONPacket.fromBuffer(buffer);
33     }
34 
35     static RCONPacket fromBuffer(ubyte[] data) @trusted {
36         auto id = data.read!(int, Endian.littleEndian);
37         auto type = data.read!(Type, Endian.littleEndian);
38         string message = null;
39 
40         // Some messages are plain empty, including no \0 at the end
41         if (data.length > 2) {
42             // TODO: encode?
43             message = cast(string)data[0..data.length - 3];
44         }
45 
46         return RCONPacket(id, type, message);
47     }
48 
49     unittest {
50         auto data =
51             nativeToLittleEndian(5) ~
52             nativeToLittleEndian(3) ~
53             cast(ubyte[])[0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00];
54 
55         auto packet = RCONPacket.fromBuffer(data);
56         assert(packet.id == 5);
57         assert(packet.type == 3);
58         assert(packet.message == "foo");
59     }
60 
61     this(int id, Type type, string message) {
62         this.id = id;
63         this.type = type;
64         this.message = message;
65     }
66 
67     @property int size() {
68         // Null character + header size
69         return cast(int)(message.length + 1 + 10);
70     }
71 
72     ubyte[] header() @trusted {
73         auto data = new ubyte[12];
74         data.write!(int, Endian.littleEndian)(size, 0);
75         data.write!(int, Endian.littleEndian)(id, 4);
76         data.write!(int, Endian.littleEndian)(type, 8);
77         return data;
78     }
79 
80     ubyte[] toBuffer() @trusted {
81         auto data = new ubyte[size + 4];
82         size_t offset = 0;
83 
84         data[offset..offset + 12] = header();
85         offset += 12;
86 
87         // TODO: decode?
88         data[offset..offset + message.length] = cast(ubyte[])message;
89         offset += message.length;
90 
91         data[offset..offset + 3] = [0x00, 0x00, 0x00];
92         return data;
93     }
94 
95     unittest {
96         auto packet = RCONPacket(5, Type.AUTH, "foo");
97 
98         assert(packet.toBuffer() ==
99             nativeToLittleEndian(14) ~
100             nativeToLittleEndian(5) ~
101             nativeToLittleEndian(3) ~
102             cast(ubyte[])[0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00]);
103     }
104 
105     void write(O)(O stream) if (isOutputStream!O) {
106         stream.write(toBuffer());
107     }
108 
109     string toString() {
110         return "RCON(%s, %s, %s)".format(id, type, message);
111     }
112 }