1 package net.sourceforge.rconed;
2
3 import java.io.IOException;
4 import java.net.DatagramPacket;
5 import java.net.DatagramSocket;
6 import java.net.InetAddress;
7 import java.net.SocketTimeoutException;
8
9 import net.sourceforge.rconed.exception.BadRcon;
10 import net.sourceforge.rconed.exception.UnexpectedDataException;
11
12 /**
13 * Rcon is a simple Java library for issuing RCON commands to game servers.
14 * <p/>
15 * CODRcon is an extension to the library to allow for Call of Duty based
16 * servers to be connected against. This has currently only been tested with
17 * Call of Duty 2 servers, but the communication protocol should be valid for
18 * all current Call of Duty versions.
19 * <p/>
20 * Example:
21 * <p/>
22 * response = CODRcon.send("127.0.0.1", 28960, rconPassword, "status");
23 * <p/>
24
25 * Copied bulk of original code from Rcon, making modifications where
26 * necessary for Call of Duty servers
27 * <p/>
28 * @author DeadEd, David Hayes
29 * @version 1.1
30 */
31 public class CODRcon {
32
33 private static final int RESPONSE_TIMEOUT = 2000;
34 private static final int MULTIPLE_PACKETS_TIMEOUT = 300;
35
36 /**
37 * Send the RCON request. Sends the command to the game server. This version allows for
38 * a local portless request to be made.
39 * <p/>
40 * As the communication travels via an unreliable method (at the choice of the Call of
41 * Duty developers), the order in which the message will be returned is not guaranteed,
42 * nor is the fact that you will receive a complete message. It is more advisable to call
43 * commands that result in shorter returns from the console, than commands that return
44 * longer results.
45 *
46 * @param ipStr The IP (as a String) of the machine where the RCON command will go.
47 * @param port The port of the machine where the RCON command will go.
48 * @param password The RCON password.
49 * @param command The RCON command (without the rcon prefix).
50 * @return The reponse text from the server after trying the RCON command.
51 * @throws SocketTimeoutException when there is any problem communicating with the server.
52 */
53 public static String send(String ipStr, int port, String password, String command)
54 throws SocketTimeoutException, BadRcon, UnexpectedDataException {
55
56 return send (-1, ipStr, port, password, command);
57
58 }
59
60 /**
61 * Send the RCON request. Sends the command to the game server. A port
62 * (localPort) must be opened to send the command through.
63 * <p/>
64 * As the communication travels via an unreliable method (at the choice of the Call of
65 * Duty developers), the order in which the message will be returned is not guaranteed,
66 * nor is the fact that you will receive a complete message. It is more advisable to call
67 * commands that result in shorter returns from the console, than commands that return
68 * longer results.
69 *
70 * @param localPort The port on the local machine where the RCON request can be made from.
71 * @param ipStr The IP (as a String) of the machine where the RCON command will go.
72 * @param port The port of the machine where the RCON command will go.
73 * @param password The RCON password.
74 * @param command The RCON command (without the rcon prefix).
75 * @return The reponse text from the server after trying the RCON command.
76 * @throws SocketTimeoutException when there is any problem communicating with the server.
77 */
78 public static String send(int localPort, String ipStr, int port, String password, String command)
79 throws SocketTimeoutException, BadRcon, UnexpectedDataException {
80
81 CODRconPacket[] requested = sendRequest(localPort, ipStr, port, password, command);
82
83 String response = assemblePacket(requested);
84
85 if (response.matches("Invalid password.\n")) {
86 throw new BadRcon();
87 }
88 return response;
89 }
90
91
92
93 private static DatagramPacket getDatagramPacket(String request, InetAddress inet, int port) {
94 byte first = -1;
95 byte last = 0;
96 byte[] buffer = request.getBytes();
97 byte[] commandBytes = new byte[buffer.length + 5];
98 commandBytes[0] = first;
99 commandBytes[1] = first;
100 commandBytes[2] = first;
101 commandBytes[3] = first;
102 for (int i = 0; i < buffer.length; i++) {
103 commandBytes[i + 4] = buffer[i];
104 }
105 commandBytes[buffer.length + 4] = last;
106
107 return new DatagramPacket(commandBytes, commandBytes.length, inet, port);
108 }
109
110 private static CODRconPacket[] sendRequest(int localPort, String ipStr, int port, String password,
111 String command) throws SocketTimeoutException {
112
113 DatagramSocket socket = null;
114 CODRconPacket[] resp = new CODRconPacket[128];
115
116 try {
117 if (localPort == -1) {
118 socket = new DatagramSocket();
119 } else {
120 socket = new DatagramSocket(localPort);
121 }
122 int packetSize = 1400;
123
124 InetAddress address = InetAddress.getByName(ipStr);
125 byte[] ip = address.getAddress();
126 InetAddress inet = InetAddress.getByAddress(ip);
127
128 String commandStr = "rcon \"" + password + "\" " + command;
129 DatagramPacket out = getDatagramPacket(commandStr, inet, port);
130 socket.send(out);
131
132 // get the response
133 byte[] data = new byte[packetSize];
134 DatagramPacket inPacket = new DatagramPacket(data, packetSize);
135 socket.setSoTimeout(RESPONSE_TIMEOUT);
136 socket.receive(inPacket);
137
138 resp[0] = new CODRconPacket(inPacket);
139 try {
140 // Wait for a possible multiple packets response
141 socket.setSoTimeout(MULTIPLE_PACKETS_TIMEOUT);
142 int i = 1;
143 while (true) {
144 socket.receive(inPacket);
145 resp[i++] = new CODRconPacket(inPacket);
146 }
147 } catch (SocketTimeoutException sex) {
148 // Server didn't send more packets
149 }
150
151 } catch (SocketTimeoutException sex) {
152 throw sex;
153 } catch (IOException ex) {
154 ex.printStackTrace();
155 } finally {
156 if (socket != null) {
157 socket.close();
158 }
159 }
160
161 return resp;
162 }
163
164 private static String assemblePacket(CODRconPacket[] respPacket) throws UnexpectedDataException {
165
166 String resp = "";
167
168 // TODO: inspect the headers to decide the correct order
169
170 for (int i = 0; i < respPacket.length; i++) {
171 if (respPacket[i] != null) {
172 // Verify that the packet was valid
173 if (respPacket[i].valid) {
174 resp = resp.concat(respPacket[i].data);
175 } else {
176 throw new UnexpectedDataException();
177 }
178 }
179 }
180 return resp;
181 }
182
183 }
184
185 class CODRconPacket {
186
187 /**
188 * ASCII representation of the full packet received (header included)
189 */
190 public String ascii = "";
191
192 /**
193 * The data included in the packet, header removed
194 */
195 public String data = "";
196
197 /**
198 * The full packet received (header included) in bytes
199 */
200 public byte[] bytes = new byte[1400];
201
202 /**
203 * Length of the packet
204 */
205 public int length = 0;
206
207 /**
208 * An indicator of the validity of the packet.
209 */
210 public boolean valid = true;
211
212 /**
213 * Represents a rcon response packet from the game server. A response may be split
214 * into multiple packets, so an array of RconPackets should be used.
215 *
216 * @param packet One DatagramPacket returned by the server
217 */
218 public CODRconPacket(DatagramPacket packet) {
219
220 this.ascii = new String(packet.getData(), 0, packet.getLength());
221 this.bytes = ascii.getBytes();
222 this.length = packet.getLength();
223
224 try {
225 this.data = parseResponse(packet.getData());
226 } catch (UnexpectedDataException e) {
227 this.data = "ERROR";
228 this.valid = false;
229 }
230 }
231
232 private static String parseResponse(byte[] buf) throws UnexpectedDataException{
233 String retVal = "";
234
235 /*
236 * The first four bytes of the return are always -1 (0xFF).
237 * Then follows the characters "print" plus the ascii new line (0x0A)
238 * thus the command below
239 */
240 if (buf[0] != -1 || buf[1] != -1 || buf[2] != -1 || buf[3] != -1 || buf[4] != 112 || buf[5] != 114 || buf[6] != 105 || buf[7] != 110 || buf[8] != 116 || !(buf[9] == 10 || buf[9] == 13)) {
241 throw new UnexpectedDataException();
242 }
243 int off = 10;
244 StringBuffer challenge = new StringBuffer(9995);
245 while (buf[off] != 0) {
246 challenge.append((char) (buf[off++] & 255));
247 }
248 retVal = challenge.toString();
249
250 return retVal;
251 }
252 }