be987083c5e54e3db1228f563b4ba1bf322a8918
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / server / ConnectAction.java
1 package be.nikiroo.utils.serial.server;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStreamReader;
6 import java.io.OutputStreamWriter;
7 import java.net.Socket;
8
9 import javax.net.ssl.SSLException;
10
11 import be.nikiroo.utils.CryptUtils;
12 import be.nikiroo.utils.Version;
13 import be.nikiroo.utils.serial.Exporter;
14 import be.nikiroo.utils.serial.Importer;
15
16 /**
17 * Base class used for the client/server basic handling.
18 * <p>
19 * It represents a single action: a client is expected to only execute one
20 * action, while a server is expected to execute one action for each client
21 * action.
22 *
23 * @author niki
24 */
25 abstract class ConnectAction {
26 private Socket s;
27 private boolean server;
28 private Version version;
29 private Version clientVersion;
30
31 private CryptUtils crypt;
32
33 private Object lock = new Object();
34 private BufferedReader in;
35 private OutputStreamWriter out;
36 private boolean contentToSend;
37
38 private long bytesReceived;
39 private long bytesSent;
40
41 /**
42 * Method that will be called when an action is performed on either the
43 * client or server this {@link ConnectAction} represent.
44 *
45 * @param version
46 * the counter part version
47 *
48 * @throws Exception
49 * in case of I/O error
50 */
51 abstract protected void action(Version version) throws Exception;
52
53 /**
54 * Method called when we negotiate the version with the client.
55 * <p>
56 * Thus, it is only called on the server.
57 * <p>
58 * Will return the actual server version by default.
59 *
60 * @param clientVersion
61 * the client version
62 *
63 * @return the version to send to the client
64 */
65 abstract protected Version negotiateVersion(Version clientVersion);
66
67 /**
68 * Handler called when an unexpected error occurs in the code.
69 *
70 * @param e
71 * the exception that occurred, SSLException usually denotes a
72 * crypt error
73 */
74 abstract protected void onError(Exception e);
75
76 /**
77 * Create a new {@link ConnectAction}.
78 *
79 * @param s
80 * the socket to bind to
81 * @param server
82 * TRUE for a server action, FALSE for a client action (will
83 * impact the process)
84 * @param key
85 * an optional key to encrypt all the communications (if NULL,
86 * everything will be sent in clear text)
87 * @param version
88 * the version of this client-or-server
89 */
90 protected ConnectAction(Socket s, boolean server, String key,
91 Version version) {
92 this.s = s;
93 this.server = server;
94 if (key != null) {
95 crypt = new CryptUtils(key);
96 }
97
98 if (version == null) {
99 this.version = new Version();
100 } else {
101 this.version = version;
102 }
103
104 clientVersion = new Version();
105 }
106
107 /**
108 * The version of this client-or-server.
109 *
110 * @return the version
111 */
112 public Version getVersion() {
113 return version;
114 }
115
116 /**
117 * The total amount of bytes received.
118 *
119 * @return the amount of bytes received
120 */
121 public long getBytesReceived() {
122 return bytesReceived;
123 }
124
125 /**
126 * The total amount of bytes sent.
127 *
128 * @return the amount of bytes sent
129 */
130 public long getBytesSent() {
131 return bytesSent;
132 }
133
134 /**
135 * Actually start the process (this is synchronous).
136 */
137 public void connect() {
138 try {
139 in = new BufferedReader(new InputStreamReader(s.getInputStream(),
140 "UTF-8"));
141 try {
142 out = new OutputStreamWriter(s.getOutputStream(), "UTF-8");
143 try {
144 if (server) {
145 String line;
146 try {
147 line = readLine(in);
148 } catch (SSLException e) {
149 out.write("Unauthorized\n");
150 throw e;
151 }
152
153 if (line != null && line.startsWith("VERSION ")) {
154 // "VERSION client-version" (VERSION 1.0.0)
155 Version clientVersion = new Version(
156 line.substring("VERSION ".length()));
157 this.clientVersion = clientVersion;
158 Version v = negotiateVersion(clientVersion);
159 if (v == null) {
160 v = new Version();
161 }
162
163 sendString("VERSION " + v.toString());
164 }
165
166 action(clientVersion);
167 } else {
168 String v = sendString("VERSION " + version.toString());
169 if (v != null && v.startsWith("VERSION ")) {
170 v = v.substring("VERSION ".length());
171 }
172
173 action(new Version(v));
174 }
175 } finally {
176 out.close();
177 out = null;
178 }
179 } finally {
180 in.close();
181 in = null;
182 }
183 } catch (Exception e) {
184 onError(e);
185 } finally {
186 try {
187 s.close();
188 } catch (Exception e) {
189 onError(e);
190 }
191 }
192 }
193
194 /**
195 * Serialise and send the given object to the counter part (and, only for
196 * client, return the deserialised answer -- the server will always receive
197 * NULL).
198 *
199 * @param data
200 * the data to send
201 *
202 * @return the answer (which can be NULL) if this action is a client, always
203 * NULL if it is a server
204 *
205 * @throws IOException
206 * in case of I/O error
207 * @throws NoSuchFieldException
208 * if the serialised data contains information about a field
209 * which does actually not exist in the class we know of
210 * @throws NoSuchMethodException
211 * if a class described in the serialised data cannot be created
212 * because it is not compatible with this code
213 * @throws ClassNotFoundException
214 * if a class described in the serialised data cannot be found
215 */
216 protected Object sendObject(Object data) throws IOException,
217 NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
218 synchronized (lock) {
219 String rep = sendString(new Exporter().append(data).toString(true,
220 true));
221 if (rep != null) {
222 return new Importer().read(rep).getValue();
223 }
224
225 return null;
226 }
227 }
228
229 /**
230 * Reserved for the server: flush the data to the client and retrieve its
231 * answer.
232 * <p>
233 * Also used internally for the client (only do something if there is
234 * contentToSend).
235 * <p>
236 * Will only flush the data if there is contentToSend.
237 *
238 * @return the deserialised answer (which can actually be NULL)
239 *
240 * @throws IOException
241 * in case of I/O error
242 * @throws NoSuchFieldException
243 * if the serialised data contains information about a field
244 * which does actually not exist in the class we know of
245 * @throws NoSuchMethodException
246 * if a class described in the serialised data cannot be created
247 * because it is not compatible with this code
248 * @throws ClassNotFoundException
249 * if a class described in the serialised data cannot be found
250 * @throws java.lang.NullPointerException
251 * if the counter part has no data to send
252 */
253 protected Object recObject() throws IOException, NoSuchFieldException,
254 NoSuchMethodException, ClassNotFoundException,
255 java.lang.NullPointerException {
256 String str = recString();
257 if (str == null) {
258 throw new NullPointerException("No more data available");
259 }
260
261 return new Importer().read(str).getValue();
262 }
263
264 /**
265 * Send the given string to the counter part (and, only for client, return
266 * the answer -- the server will always receive NULL).
267 *
268 * @param line
269 * the data to send (we will add a line feed)
270 *
271 * @return the answer if this action is a client (without the added line
272 * feed), NULL if it is a server
273 *
274 * @throws IOException
275 * in case of I/O error
276 * @throws SSLException
277 * in case of crypt error
278 */
279 protected String sendString(String line) throws IOException {
280 synchronized (lock) {
281 writeLine(out, line);
282
283 if (server) {
284 out.flush();
285 return null;
286 }
287
288 contentToSend = true;
289 return recString();
290 }
291 }
292
293 /**
294 * Reserved for the server (externally): flush the data to the client and
295 * retrieve its answer.
296 * <p>
297 * Also used internally for the client (only do something if there is
298 * contentToSend).
299 * <p>
300 * Will only flush the data if there is contentToSend.
301 *
302 * @return the answer (which can be NULL)
303 *
304 * @throws IOException
305 * in case of I/O error
306 * @throws SSLException
307 * in case of crypt error
308 */
309 protected String recString() throws IOException {
310 synchronized (lock) {
311 if (server || contentToSend) {
312 if (contentToSend) {
313 out.flush();
314 contentToSend = false;
315 }
316
317 return readLine(in);
318 }
319
320 return null;
321 }
322 }
323
324 /**
325 * Read a possibly encrypted line.
326 *
327 * @param in
328 * the stream to read from
329 * @return the unencrypted line
330 *
331 *
332 * @throws IOException
333 * in case of I/O error
334 * @throws SSLException
335 * in case of crypt error
336 */
337 private String readLine(BufferedReader in) throws IOException {
338 String line = in.readLine();
339 if (line != null) {
340 bytesReceived += line.length();
341 if (crypt != null) {
342 line = crypt.decrypt64s(line, false);
343 }
344 }
345
346 return line;
347 }
348
349 /**
350 * Write a line, possible encrypted.
351 *
352 * @param out
353 * the stream to write to
354 * @param line
355 * the line to write
356 * @throws IOException
357 * in case of I/O error
358 * @throws SSLException
359 * in case of crypt error
360 */
361 private void writeLine(OutputStreamWriter out, String line)
362 throws IOException {
363 if (crypt == null) {
364 out.write(line);
365 bytesSent += line.length();
366 } else {
367 // TODO: how NOT to create so many big Strings?
368 String b64 = crypt.encrypt64(line, false);
369 out.write(b64);
370 bytesSent += b64.length();
371 }
372 out.write("\n");
373 bytesSent++;
374 }
375 }