Merge commit '54224be8f207f5dee1d115cba658f622680a776e'
[fanfix.git] / serial / server / ConnectAction.java
1 package be.nikiroo.utils.serial.server;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.OutputStream;
6 import java.net.Socket;
7
8 import javax.net.ssl.SSLException;
9
10 import be.nikiroo.utils.CryptUtils;
11 import be.nikiroo.utils.IOUtils;
12 import be.nikiroo.utils.StringUtils;
13 import be.nikiroo.utils.Version;
14 import be.nikiroo.utils.serial.Exporter;
15 import be.nikiroo.utils.serial.Importer;
16 import be.nikiroo.utils.streams.BufferedOutputStream;
17 import be.nikiroo.utils.streams.NextableInputStream;
18 import be.nikiroo.utils.streams.NextableInputStreamStep;
19 import be.nikiroo.utils.streams.ReplaceInputStream;
20 import be.nikiroo.utils.streams.ReplaceOutputStream;
21
22 /**
23 * Base class used for the client/server basic handling.
24 * <p>
25 * It represents a single action: a client is expected to only execute one
26 * action, while a server is expected to execute one action for each client
27 * action.
28 *
29 * @author niki
30 */
31 abstract class ConnectAction {
32 // We separate each "packet" we send with this character and make sure it
33 // does not occurs in the message itself.
34 static private char STREAM_SEP = '\b';
35 static private String[] STREAM_RAW = new String[] { "\\", "\b" };
36 static private String[] STREAM_CODED = new String[] { "\\\\", "\\b" };
37
38 private Socket s;
39 private boolean server;
40
41 private Version clientVersion;
42 private Version serverVersion;
43
44 private CryptUtils crypt;
45
46 private Object lock = new Object();
47 private NextableInputStream in;
48 private BufferedOutputStream out;
49 private boolean contentToSend;
50
51 /**
52 * Method that will be called when an action is performed on either the
53 * client or server this {@link ConnectAction} represent.
54 *
55 * @param version
56 * the version on the other side of the communication (client or
57 * server)
58 *
59 * @throws Exception
60 * in case of I/O error
61 */
62 abstract protected void action(Version version) throws Exception;
63
64 /**
65 * Method called when we negotiate the version with the client.
66 * <p>
67 * Thus, it is only called on the server.
68 * <p>
69 * Will return the actual server version by default.
70 *
71 * @param clientVersion
72 * the client version
73 *
74 * @return the version to send to the client
75 */
76 abstract protected Version negotiateVersion(Version clientVersion);
77
78 /**
79 * Handler called when an unexpected error occurs in the code.
80 *
81 * @param e
82 * the exception that occurred, SSLException usually denotes a
83 * crypt error
84 */
85 abstract protected void onError(Exception e);
86
87 /**
88 * Create a new {@link ConnectAction}.
89 *
90 * @param s
91 * the socket to bind to
92 * @param server
93 * TRUE for a server action, FALSE for a client action (will
94 * impact the process)
95 * @param key
96 * an optional key to encrypt all the communications (if NULL,
97 * everything will be sent in clear text)
98 * @param version
99 * the client-or-server version (depending upon the boolean
100 * parameter <tt>server</tt>)
101 */
102 protected ConnectAction(Socket s, boolean server, String key,
103 Version version) {
104 this.s = s;
105 this.server = server;
106 if (key != null) {
107 crypt = new CryptUtils(key);
108 }
109
110 if (version == null) {
111 version = new Version();
112 }
113
114 if (server) {
115 serverVersion = version;
116 } else {
117 clientVersion = version;
118 }
119 }
120
121 /**
122 * The version of this client-or-server.
123 *
124 * @return the version
125 */
126 public Version getVersion() {
127 if (server) {
128 return serverVersion;
129 }
130
131 return clientVersion;
132 }
133
134 /**
135 * The total amount of bytes received.
136 *
137 * @return the amount of bytes received
138 */
139 public long getBytesReceived() {
140 return in.getBytesRead();
141 }
142
143 /**
144 * The total amount of bytes sent.
145 *
146 * @return the amount of bytes sent
147 */
148 public long getBytesWritten() {
149 return out.getBytesWritten();
150 }
151
152 /**
153 * Actually start the process (this is synchronous).
154 */
155 public void connect() {
156 try {
157 in = new NextableInputStream(s.getInputStream(),
158 new NextableInputStreamStep(STREAM_SEP));
159 try {
160 out = new BufferedOutputStream(s.getOutputStream());
161 try {
162 // Negotiate version
163 Version version;
164 if (server) {
165 String HELLO = recString();
166 if (HELLO == null || !HELLO.startsWith("VERSION ")) {
167 throw new SSLException(
168 "Client used bad encryption key");
169 }
170 version = negotiateVersion(new Version(
171 HELLO.substring("VERSION ".length())));
172 sendString("VERSION " + version);
173 } else {
174 String HELLO = sendString("VERSION " + clientVersion);
175 if (HELLO == null || !HELLO.startsWith("VERSION ")) {
176 throw new SSLException(
177 "Server did not accept the encryption key");
178 }
179 version = new Version(HELLO.substring("VERSION "
180 .length()));
181 }
182
183 // Actual code
184 action(version);
185 } finally {
186 out.close();
187 }
188 } finally {
189 in.close();
190 }
191 } catch (Exception e) {
192 onError(e);
193 } finally {
194 try {
195 s.close();
196 } catch (Exception e) {
197 onError(e);
198 }
199 }
200 }
201
202 /**
203 * Serialise and send the given object to the counter part (and, only for
204 * client, return the deserialised answer -- the server will always receive
205 * NULL).
206 *
207 * @param data
208 * the data to send
209 *
210 * @return the answer (which can be NULL if no answer, or NULL for an answer
211 * which is NULL) if this action is a client, always NULL if it is a
212 * server
213 *
214 * @throws IOException
215 * in case of I/O error
216 * @throws NoSuchFieldException
217 * if the serialised data contains information about a field
218 * which does actually not exist in the class we know of
219 * @throws NoSuchMethodException
220 * if a class described in the serialised data cannot be created
221 * because it is not compatible with this code
222 * @throws ClassNotFoundException
223 * if a class described in the serialised data cannot be found
224 */
225 protected Object sendObject(Object data) throws IOException,
226 NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
227 return send(out, data, false);
228 }
229
230 /**
231 * Reserved for the server: flush the data to the client and retrieve its
232 * answer.
233 * <p>
234 * Also used internally for the client (only do something if there is
235 * contentToSend).
236 * <p>
237 * Will only flush the data if there is contentToSend.
238 *
239 * @return the deserialised answer (which can actually be NULL)
240 *
241 * @throws IOException
242 * in case of I/O error
243 * @throws NoSuchFieldException
244 * if the serialised data contains information about a field
245 * which does actually not exist in the class we know of
246 * @throws NoSuchMethodException
247 * if a class described in the serialised data cannot be created
248 * because it is not compatible with this code
249 * @throws ClassNotFoundException
250 * if a class described in the serialised data cannot be found
251 * @throws java.lang.NullPointerException
252 * if the counter part has no data to send
253 */
254 protected Object recObject() throws IOException, NoSuchFieldException,
255 NoSuchMethodException, ClassNotFoundException,
256 java.lang.NullPointerException {
257 return rec(false);
258 }
259
260 /**
261 * Send the given string to the counter part (and, only for client, return
262 * the answer -- the server will always receive NULL).
263 *
264 * @param line
265 * the data to send (we will add a line feed)
266 *
267 * @return the answer if this action is a client (without the added line
268 * feed), NULL if it is a server
269 *
270 * @throws IOException
271 * in case of I/O error
272 * @throws SSLException
273 * in case of crypt error
274 */
275 protected String sendString(String line) throws IOException {
276 try {
277 return (String) send(out, line, true);
278 } catch (NoSuchFieldException e) {
279 // Cannot happen
280 e.printStackTrace();
281 } catch (NoSuchMethodException e) {
282 // Cannot happen
283 e.printStackTrace();
284 } catch (ClassNotFoundException e) {
285 // Cannot happen
286 e.printStackTrace();
287 }
288
289 return null;
290 }
291
292 /**
293 * Reserved for the server (externally): flush the data to the client and
294 * retrieve its answer.
295 * <p>
296 * Also used internally for the client (only do something if there is
297 * contentToSend).
298 * <p>
299 * Will only flush the data if there is contentToSend.
300 *
301 * @return the answer (which can be NULL if no more content)
302 *
303 * @throws IOException
304 * in case of I/O error
305 * @throws SSLException
306 * in case of crypt error
307 */
308 protected String recString() throws IOException {
309 try {
310 return (String) rec(true);
311 } catch (NoSuchFieldException e) {
312 // Cannot happen
313 e.printStackTrace();
314 } catch (NoSuchMethodException e) {
315 // Cannot happen
316 e.printStackTrace();
317 } catch (ClassNotFoundException e) {
318 // Cannot happen
319 e.printStackTrace();
320 } catch (NullPointerException e) {
321 // Should happen
322 e.printStackTrace();
323 }
324
325 return null;
326 }
327
328 /**
329 * Serialise and send the given object to the counter part (and, only for
330 * client, return the deserialised answer -- the server will always receive
331 * NULL).
332 *
333 * @param out
334 * the stream to write to
335 * @param data
336 * the data to write
337 * @param asString
338 * TRUE to write it as a String, FALSE to write it as an Object
339 *
340 * @return the answer (which can be NULL if no answer, or NULL for an answer
341 * which is NULL) if this action is a client, always NULL if it is a
342 * server
343 *
344 * @throws IOException
345 * in case of I/O error
346 * @throws SSLException
347 * in case of crypt error
348 * @throws IOException
349 * in case of I/O error
350 * @throws NoSuchFieldException
351 * if the serialised data contains information about a field
352 * which does actually not exist in the class we know of
353 * @throws NoSuchMethodException
354 * if a class described in the serialised data cannot be created
355 * because it is not compatible with this code
356 * @throws ClassNotFoundException
357 * if a class described in the serialised data cannot be found
358 */
359 private Object send(BufferedOutputStream out, Object data, boolean asString)
360 throws IOException, NoSuchFieldException, NoSuchMethodException,
361 ClassNotFoundException, java.lang.NullPointerException {
362
363 synchronized (lock) {
364 OutputStream sub;
365 if (crypt != null) {
366 sub = crypt.encrypt64(out.open());
367 } else {
368 sub = out.open();
369 }
370
371 sub = new ReplaceOutputStream(sub, STREAM_RAW, STREAM_CODED);
372 try {
373 if (asString) {
374 sub.write(StringUtils.getBytes(data.toString()));
375 } else {
376 new Exporter(sub).append(data);
377 }
378 } finally {
379 sub.close();
380 }
381
382 out.write(STREAM_SEP);
383
384 if (server) {
385 out.flush();
386 return null;
387 }
388
389 contentToSend = true;
390 try {
391 return rec(asString);
392 } catch (NullPointerException e) {
393 // We accept no data here for Objects
394 }
395
396 return null;
397 }
398 }
399
400 /**
401 * Reserved for the server: flush the data to the client and retrieve its
402 * answer.
403 * <p>
404 * Also used internally for the client (only do something if there is
405 * contentToSend).
406 * <p>
407 * Will only flush the data if there is contentToSend.
408 * <p>
409 * Note that the behaviour is slightly different for String and Object
410 * reading regarding exceptions:
411 * <ul>
412 * <li>NULL means that the counter part has no more data to send</li>
413 * <li>All the exceptions except {@link IOException} are there for Object
414 * conversion</li>
415 * </ul>
416 *
417 * @param asString
418 * TRUE for String reading, FALSE for Object reading (which can
419 * still be a String)
420 *
421 * @return the deserialised answer (which can actually be NULL)
422 *
423 * @throws IOException
424 * in case of I/O error
425 * @throws NoSuchFieldException
426 * if the serialised data contains information about a field
427 * which does actually not exist in the class we know of
428 * @throws NoSuchMethodException
429 * if a class described in the serialised data cannot be created
430 * because it is not compatible with this code
431 * @throws ClassNotFoundException
432 * if a class described in the serialised data cannot be found
433 * @throws java.lang.NullPointerException
434 * for Objects only: if the counter part has no data to send
435 */
436 @SuppressWarnings("resource")
437 private Object rec(boolean asString) throws IOException,
438 NoSuchFieldException, NoSuchMethodException,
439 ClassNotFoundException, java.lang.NullPointerException {
440
441 synchronized (lock) {
442 if (server || contentToSend) {
443 if (contentToSend) {
444 out.flush();
445 contentToSend = false;
446 }
447
448 if (in.next() && !in.eof()) {
449 InputStream read = new ReplaceInputStream(in.open(),
450 STREAM_CODED, STREAM_RAW);
451 try {
452 if (crypt != null) {
453 read = crypt.decrypt64(read);
454 }
455
456 if (asString) {
457 return IOUtils.readSmallStream(read);
458 }
459
460 return new Importer().read(read).getValue();
461 } finally {
462 read.close();
463 }
464 }
465
466 if (!asString) {
467 throw new NullPointerException();
468 }
469 }
470
471 return null;
472 }
473 }
474 }