8dbc7aa22ce195383f2c82cd225dd9f042e25b14
[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 new Exporter(out).append(data);
221
222 if (server) {
223 out.flush();
224 return null;
225 }
226
227 contentToSend = true;
228 try {
229 return recObject();
230 } catch (NullPointerException e) {
231 // We accept no data here
232 }
233
234 return null;
235 }
236 }
237
238 /**
239 * Reserved for the server: flush the data to the client and retrieve its
240 * answer.
241 * <p>
242 * Also used internally for the client (only do something if there is
243 * contentToSend).
244 * <p>
245 * Will only flush the data if there is contentToSend.
246 *
247 * @return the deserialised answer (which can actually be NULL)
248 *
249 * @throws IOException
250 * in case of I/O error
251 * @throws NoSuchFieldException
252 * if the serialised data contains information about a field
253 * which does actually not exist in the class we know of
254 * @throws NoSuchMethodException
255 * if a class described in the serialised data cannot be created
256 * because it is not compatible with this code
257 * @throws ClassNotFoundException
258 * if a class described in the serialised data cannot be found
259 * @throws java.lang.NullPointerException
260 * if the counter part has no data to send
261 */
262 protected Object recObject() throws IOException, NoSuchFieldException,
263 NoSuchMethodException, ClassNotFoundException,
264 java.lang.NullPointerException {
265 synchronized (lock) {
266 if (server || contentToSend) {
267 if (contentToSend) {
268 out.flush();
269 contentToSend = false;
270 }
271
272 return new Importer().read(in).getValue();
273 }
274
275 return null;
276 }
277 }
278
279 /**
280 * Send the given string to the counter part (and, only for client, return
281 * the answer -- the server will always receive NULL).
282 *
283 * @param line
284 * the data to send (we will add a line feed)
285 *
286 * @return the answer if this action is a client (without the added line
287 * feed), NULL if it is a server
288 *
289 * @throws IOException
290 * in case of I/O error
291 * @throws SSLException
292 * in case of crypt error
293 */
294 protected String sendString(String line) throws IOException {
295 synchronized (lock) {
296 writeLine(out, line);
297
298 if (server) {
299 out.flush();
300 return null;
301 }
302
303 contentToSend = true;
304 return recString();
305 }
306 }
307
308 /**
309 * Reserved for the server (externally): flush the data to the client and
310 * retrieve its answer.
311 * <p>
312 * Also used internally for the client (only do something if there is
313 * contentToSend).
314 * <p>
315 * Will only flush the data if there is contentToSend.
316 *
317 * @return the answer (which can be NULL)
318 *
319 * @throws IOException
320 * in case of I/O error
321 * @throws SSLException
322 * in case of crypt error
323 */
324 protected String recString() throws IOException {
325 synchronized (lock) {
326 if (server || contentToSend) {
327 if (contentToSend) {
328 out.flush();
329 contentToSend = false;
330 }
331
332 return readLine(in);
333 }
334
335 return null;
336 }
337 }
338
339 /**
340 * Read a possibly encrypted line.
341 *
342 * @param in
343 * the stream to read from
344 * @return the unencrypted line
345 *
346 *
347 * @throws IOException
348 * in case of I/O error
349 * @throws SSLException
350 * in case of crypt error
351 */
352 private String readLine(InputStream in) throws IOException {
353 if (inReader == null) {
354 inReader = new BufferedReader(new InputStreamReader(in));
355 }
356 String line = inReader.readLine();
357 if (line != null) {
358 bytesReceived += line.length();
359 if (crypt != null) {
360 line = crypt.decrypt64s(line, false);
361 }
362 }
363
364 return line;
365 }
366
367 private BufferedReader inReader;
368
369 /**
370 * Write a line, possible encrypted.
371 *
372 * @param out
373 * the stream to write to
374 * @param line
375 * the line to write
376 * @throws IOException
377 * in case of I/O error
378 * @throws SSLException
379 * in case of crypt error
380 */
381 private void writeLine(OutputStream out, String line) throws IOException {
382 if (crypt == null) {
383 out.write(line.getBytes("UTF-8"));
384 bytesSent += line.length();
385 } else {
386 // TODO: how NOT to create so many big Strings?
387 String b64 = crypt.encrypt64(line, false);
388 out.write(b64.getBytes("UTF-8"));
389 bytesSent += b64.length();
390 }
391 out.write("\n".getBytes("UTF-8"));
392 bytesSent++;
393 }
394 }