server: string->stream, step 1
[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 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(InputStream in) throws IOException {
338 if (inReader == null) {
339 inReader = new BufferedReader(new InputStreamReader(in));
340 }
341 String line = inReader.readLine();
342 if (line != null) {
343 bytesReceived += line.length();
344 if (crypt != null) {
345 line = crypt.decrypt64s(line, false);
346 }
347 }
348
349 return line;
350 }
351
352 private BufferedReader inReader;
353
354 /**
355 * Write a line, possible encrypted.
356 *
357 * @param out
358 * the stream to write to
359 * @param line
360 * the line to write
361 * @throws IOException
362 * in case of I/O error
363 * @throws SSLException
364 * in case of crypt error
365 */
366 private void writeLine(OutputStream out, String line) throws IOException {
367 if (crypt == null) {
368 out.write(line.getBytes());
369 bytesSent += line.length();
370 } else {
371 // TODO: how NOT to create so many big Strings?
372 String b64 = crypt.encrypt64(line, false);
373 out.write(b64.getBytes());
374 bytesSent += b64.length();
375 }
376 out.write("\n".getBytes());
377 bytesSent++;
378 }
379 }