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