1 module mcrcd;
2 
3 import std.string;
4 import std.socket;
5 import std.bitmanip;
6 import core.time;
7 
8 /// Response for Rcon packets.
9 struct MCRconResponse
10 {
11 	///
12 	this(ubyte[] _data, int _responseID = -1)
13 	{
14 		data = _data;
15 		responseID = _responseID;
16 	}
17 
18 	/// Response ID for packets. `-1` on login error.
19 	int responseID;
20 
21 	/// Automatically removes all § codes from text.
22 	@property string unformatted() const
23 	{
24 		string raw = text.idup;
25 		size_t index;
26 		while((index = raw.indexOf('§')) != -1)
27 		{
28 			if(index < raw.length - 2)
29 				raw = raw[0 .. index] ~ raw[index + 3 .. $];
30 			else if(index < raw.length - 1)
31 				raw = raw[0 .. index] ~ raw[index + 2 .. $];
32 			else
33 				break;
34 		}
35 		return raw;
36 	}
37 
38 	union
39 	{
40 		ubyte[] data; /// Raw data containing response.
41 		char[] text; /// ditto
42 	}
43 }
44 
45 unittest
46 {
47 	MCRconResponse res;
48 	res.text = "foo§".dup;
49 	assert(res.unformatted == "foo");
50 	res.text = "foo§f".dup;
51 	assert(res.unformatted == "foo");
52 }
53 
54 ///
55 enum MCRconPacket : int
56 {
57 	Command = 2, ///
58 	Login = 3 ///
59 }
60 
61 /// Rcon class for connections.
62 class MCRcon
63 {
64 private:
65 	Socket _socket;
66 public:
67 	/// Connects to `host:port` and creates a new socket.
68 	void connect(string host, short port)
69 	{
70 		assert(!isConnected, "Still connected!");
71 		_socket = new TcpSocket();
72 		_socket.connect(new InternetAddress(host, port));
73 	}
74 
75 	/// Disconnects from the server.
76 	void disconnect()
77 	{
78 		_socket.close();
79 		_socket = null;
80 	}
81 
82 	/// Returns if still connected to the server.
83 	@property bool isConnected()
84 	{
85 		return _socket !is null && _socket.isAlive();
86 	}
87 
88 	/// Sends a packet containing `data` with `packetID` as ID and returns the data synchronously.
89 	MCRconResponse send(MCRconPacket packetID, string data)
90 	{
91 		assert(isConnected, "Cannot send data without being connected");
92 		ubyte[] payload = cast(ubyte[])[0, 0, 0, 0] ~ cast(ubyte[])nativeToLittleEndian(cast(int)packetID) ~ cast(ubyte[])data ~ cast(ubyte[])[0, 0];
93 		assert(_socket.send(cast(ubyte[])nativeToLittleEndian(cast(int)payload.length) ~ payload) != Socket.ERROR, "Couldn't send packet! " ~ _socket.getErrorText());
94 
95 		MCRconResponse response;
96 
97 		while(true)
98 		{
99 			ubyte[4] recv = new ubyte[4];
100 			assert(_socket.receive(recv) > 0, "Couldn't receive packet! " ~ _socket.getErrorText());
101 			int packLength = littleEndianToNative!int(recv);
102 
103 			ubyte[] packet = new ubyte[packLength];
104 			assert(_socket.receive(packet) > 0, "Couldn't receive packet! " ~ _socket.getErrorText());
105 
106 			response.responseID = littleEndianToNative!int(packet[0 .. 4]);
107 			int packetType = littleEndianToNative!int(packet[4 .. 8]);
108 			response.data ~= packet[8 .. $ - 2];
109 			assert(packet[$ - 2 .. $] == cast(ubyte[])[0, 0], "Invalid padding");
110 			assert(response.responseID != -1, "Login failed");
111 
112 			auto sockIn = new SocketSet(1);
113 			sockIn.add(_socket);
114 
115 			if(Socket.select(sockIn, new SocketSet(0), new SocketSet(0), 0.msecs) == 0)
116 				return response;
117 		}
118 	}
119 
120 	/// Shorthand for `send(MCRconPacket.Command, command)`
121 	auto command(string command)
122 	{
123 		return send(MCRconPacket.Command, command);
124 	}
125 
126 	/// Shorthand for `send(MCRconPacket.Login, password)`
127 	auto login(string password)
128 	{
129 		return send(MCRconPacket.Login, password);
130 	}
131 }