Merge commit 'b459e462a5c3447d0693674253c40bc7385a4f66'
[fanfix.git] / serial / server / ConnectAction.java
CommitLineData
79ce1a49 1package be.nikiroo.utils.serial.server;
ce0974c4 2
ce0974c4 3import java.io.IOException;
9af739b5 4import java.io.InputStream;
9af739b5 5import java.io.OutputStream;
ce0974c4
NR
6import java.net.Socket;
7
9fb03c36
NR
8import javax.net.ssl.SSLException;
9
8468bb79 10import be.nikiroo.utils.CryptUtils;
08f80ac5 11import be.nikiroo.utils.IOUtils;
f8147a0e 12import be.nikiroo.utils.StringUtils;
3087aeb5 13import be.nikiroo.utils.Version;
79ce1a49
NR
14import be.nikiroo.utils.serial.Exporter;
15import be.nikiroo.utils.serial.Importer;
08f80ac5
NR
16import be.nikiroo.utils.streams.BufferedOutputStream;
17import be.nikiroo.utils.streams.NextableInputStream;
18import be.nikiroo.utils.streams.NextableInputStreamStep;
19import be.nikiroo.utils.streams.ReplaceInputStream;
20import be.nikiroo.utils.streams.ReplaceOutputStream;
ce0974c4 21
f157aed8
NR
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 */
ce0974c4 31abstract class ConnectAction {
579c8b90
NR
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
ce0974c4
NR
38 private Socket s;
39 private boolean server;
ce0974c4 40
3087aeb5
NR
41 private Version clientVersion;
42 private Version serverVersion;
43
8468bb79
NR
44 private CryptUtils crypt;
45
ce0974c4 46 private Object lock = new Object();
08f80ac5
NR
47 private NextableInputStream in;
48 private BufferedOutputStream out;
ce0974c4
NR
49 private boolean contentToSend;
50
f157aed8
NR
51 /**
52 * Method that will be called when an action is performed on either the
53 * client or server this {@link ConnectAction} represent.
54 *
3087aeb5
NR
55 * @param version
56 * the version on the other side of the communication (client or
57 * server)
58 *
f157aed8
NR
59 * @throws Exception
60 * in case of I/O error
61 */
3087aeb5
NR
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);
f157aed8
NR
77
78 /**
79 * Handler called when an unexpected error occurs in the code.
80 *
81 * @param e
9fb03c36
NR
82 * the exception that occurred, SSLException usually denotes a
83 * crypt error
f157aed8
NR
84 */
85 abstract protected void onError(Exception e);
ce0974c4 86
f157aed8
NR
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)
8468bb79
NR
95 * @param key
96 * an optional key to encrypt all the communications (if NULL,
97 * everything will be sent in clear text)
3087aeb5
NR
98 * @param version
99 * the client-or-server version (depending upon the boolean
100 * parameter <tt>server</tt>)
f157aed8 101 */
3087aeb5
NR
102 protected ConnectAction(Socket s, boolean server, String key,
103 Version version) {
ce0974c4
NR
104 this.s = s;
105 this.server = server;
8468bb79
NR
106 if (key != null) {
107 crypt = new CryptUtils(key);
108 }
3087aeb5
NR
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;
ce0974c4
NR
132 }
133
4bb7e88e
NR
134 /**
135 * The total amount of bytes received.
136 *
137 * @return the amount of bytes received
138 */
139 public long getBytesReceived() {
08f80ac5 140 return in.getBytesRead();
4bb7e88e
NR
141 }
142
143 /**
144 * The total amount of bytes sent.
145 *
146 * @return the amount of bytes sent
147 */
08f80ac5
NR
148 public long getBytesWritten() {
149 return out.getBytesWritten();
4bb7e88e
NR
150 }
151
f157aed8
NR
152 /**
153 * Actually start the process (this is synchronous).
154 */
ce0974c4
NR
155 public void connect() {
156 try {
08f80ac5 157 in = new NextableInputStream(s.getInputStream(),
579c8b90 158 new NextableInputStreamStep(STREAM_SEP));
08f80ac5
NR
159 try {
160 out = new BufferedOutputStream(s.getOutputStream());
08f80ac5 161 try {
5cb2cda7
NR
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);
ce0974c4
NR
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
f157aed8
NR
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 *
08f80ac5
NR
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
f157aed8
NR
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 */
4bb7e88e
NR
225 protected Object sendObject(Object data) throws IOException,
226 NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
08f80ac5 227 return send(out, data, false);
ce0974c4
NR
228 }
229
f157aed8
NR
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 */
4bb7e88e
NR
254 protected Object recObject() throws IOException, NoSuchFieldException,
255 NoSuchMethodException, ClassNotFoundException,
256 java.lang.NullPointerException {
08f80ac5 257 return rec(false);
ce0974c4
NR
258 }
259
cd0c27d2 260 /**
f157aed8
NR
261 * Send the given string to the counter part (and, only for client, return
262 * the answer -- the server will always receive NULL).
cd0c27d2 263 *
f157aed8
NR
264 * @param line
265 * the data to send (we will add a line feed)
cd0c27d2 266 *
f157aed8
NR
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
9fb03c36
NR
272 * @throws SSLException
273 * in case of crypt error
cd0c27d2 274 */
f157aed8 275 protected String sendString(String line) throws IOException {
08f80ac5
NR
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();
ce0974c4 287 }
08f80ac5
NR
288
289 return null;
ce0974c4
NR
290 }
291
f157aed8
NR
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 *
08f80ac5 301 * @return the answer (which can be NULL if no more content)
f157aed8
NR
302 *
303 * @throws IOException
304 * in case of I/O error
9fb03c36
NR
305 * @throws SSLException
306 * in case of crypt error
f157aed8 307 */
79ce1a49 308 protected String recString() throws IOException {
08f80ac5
NR
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();
ce0974c4 323 }
08f80ac5
NR
324
325 return null;
ce0974c4 326 }
8468bb79 327
9fb03c36 328 /**
08f80ac5
NR
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).
9fb03c36 332 *
08f80ac5
NR
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
9fb03c36 339 *
08f80ac5
NR
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
9fb03c36
NR
343 *
344 * @throws IOException
345 * in case of I/O error
346 * @throws SSLException
347 * in case of crypt error
08f80ac5
NR
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
9fb03c36 358 */
08f80ac5
NR
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;
8468bb79 365 if (crypt != null) {
a6a73de3 366 sub = crypt.encrypt64(out.open());
08f80ac5
NR
367 } else {
368 sub = out.open();
8468bb79 369 }
8468bb79 370
579c8b90 371 sub = new ReplaceOutputStream(sub, STREAM_RAW, STREAM_CODED);
08f80ac5
NR
372 try {
373 if (asString) {
f8147a0e 374 sub.write(StringUtils.getBytes(data.toString()));
08f80ac5
NR
375 } else {
376 new Exporter(sub).append(data);
377 }
378 } finally {
379 sub.close();
380 }
8468bb79 381
579c8b90 382 out.write(STREAM_SEP);
08f80ac5
NR
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 }
9af739b5 399
9fb03c36 400 /**
08f80ac5
NR
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)
9fb03c36 422 *
9fb03c36
NR
423 * @throws IOException
424 * in case of I/O error
08f80ac5
NR
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
9fb03c36 435 */
bd86c221 436 @SuppressWarnings("resource")
08f80ac5
NR
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
d2219aa0 448 if (in.next() && !in.eof()) {
579c8b90
NR
449 InputStream read = new ReplaceInputStream(in.open(),
450 STREAM_CODED, STREAM_RAW);
08f80ac5
NR
451 try {
452 if (crypt != null) {
a6a73de3 453 read = crypt.decrypt64(read);
08f80ac5
NR
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;
8468bb79 472 }
8468bb79 473 }
ce0974c4 474}