update changelog
[fanfix.git] / src / be / nikiroo / utils / serial / server / Server.java
1 package be.nikiroo.utils.serial.server;
2
3 import java.io.IOException;
4 import java.net.ServerSocket;
5 import java.net.Socket;
6 import java.net.UnknownHostException;
7 import java.util.ArrayList;
8 import java.util.List;
9
10 import javax.net.ssl.SSLServerSocket;
11 import javax.net.ssl.SSLServerSocketFactory;
12 import javax.net.ssl.SSLSocket;
13 import javax.net.ssl.SSLSocketFactory;
14
15 import be.nikiroo.utils.TraceHandler;
16
17 /**
18 * This class implements a simple server that can listen for connections and
19 * send/receive objects.
20 * <p>
21 * Note: this {@link Server} has to be discarded after use (cannot be started
22 * twice).
23 *
24 * @author niki
25 */
26 abstract class Server implements Runnable {
27 static private final String[] ANON_CIPHERS = getAnonCiphers();
28
29 private final String name;
30 private final boolean ssl;
31 private final Object lock = new Object();
32 private final Object counterLock = new Object();
33
34 private ServerSocket ss;
35 private int port;
36
37 private boolean started;
38 private boolean exiting = false;
39 private int counter;
40
41 private long bytesReceived;
42 private long bytesSent;
43
44 private TraceHandler tracer = new TraceHandler();
45
46 /**
47 * Create a new {@link ConnectActionServer} to handle a request.
48 *
49 * @param s
50 * the socket to service
51 *
52 * @return the action
53 */
54 abstract ConnectActionServer createConnectActionServer(Socket s);
55
56 /**
57 * Create a new server that will start listening on the network when
58 * {@link Server#start()} is called.
59 *
60 * @param port
61 * the port to listen on, or 0 to assign any unallocated port
62 * found (which can later on be queried via
63 * {@link Server#getPort()}
64 * @param ssl
65 * use a SSL connection (or not)
66 *
67 * @throws IOException
68 * in case of I/O error
69 * @throws UnknownHostException
70 * if the IP address of the host could not be determined
71 * @throws IllegalArgumentException
72 * if the port parameter is outside the specified range of valid
73 * port values, which is between 0 and 65535, inclusive
74 */
75 public Server(int port, boolean ssl) throws IOException {
76 this((String) null, port, ssl);
77 }
78
79 /**
80 * Create a new server that will start listening on the network when
81 * {@link Server#start()} is called.
82 *
83 * @param name
84 * the server name (only used for debug info and traces)
85 * @param port
86 * the port to listen on
87 * @param ssl
88 * use a SSL connection (or not)
89 *
90 * @throws IOException
91 * in case of I/O error
92 * @throws UnknownHostException
93 * if the IP address of the host could not be determined
94 * @throws IllegalArgumentException
95 * if the port parameter is outside the specified range of valid
96 * port values, which is between 0 and 65535, inclusive
97 */
98 public Server(String name, int port, boolean ssl) throws IOException {
99 this.name = name;
100 this.port = port;
101 this.ssl = ssl;
102 this.ss = createSocketServer(port, ssl);
103
104 if (this.port == 0) {
105 this.port = this.ss.getLocalPort();
106 }
107 }
108
109 /**
110 * The traces handler for this {@link Server}.
111 *
112 * @return the traces handler
113 */
114 public TraceHandler getTraceHandler() {
115 return tracer;
116 }
117
118 /**
119 * The traces handler for this {@link Server}.
120 *
121 * @param tracer
122 * the new traces handler
123 */
124 public void setTraceHandler(TraceHandler tracer) {
125 if (tracer == null) {
126 tracer = new TraceHandler(false, false, false);
127 }
128
129 this.tracer = tracer;
130 }
131
132 /**
133 * The name of this {@link Server} if any.
134 * <p>
135 * Used for traces and debug purposes only.
136 *
137 * @return the name or NULL
138 */
139 public String getName() {
140 return name;
141 }
142
143 /**
144 * Return the assigned port.
145 *
146 * @return the assigned port
147 */
148 public int getPort() {
149 return port;
150 }
151
152 /**
153 * The total amount of bytes received.
154 *
155 * @return the amount of bytes received
156 */
157 public long getBytesReceived() {
158 return bytesReceived;
159 }
160
161 /**
162 * The total amount of bytes sent.
163 *
164 * @return the amount of bytes sent
165 */
166 public long getBytesSent() {
167 return bytesSent;
168 }
169
170 /**
171 * Start the server (listen on the network for new connections).
172 * <p>
173 * Can only be called once.
174 * <p>
175 * This call is asynchronous, and will just start a new {@link Thread} on
176 * itself (see {@link Server#run()}).
177 */
178 public void start() {
179 new Thread(this).start();
180 }
181
182 /**
183 * Start the server (listen on the network for new connections).
184 * <p>
185 * Can only be called once.
186 * <p>
187 * You may call it via {@link Server#start()} for an asynchronous call, too.
188 */
189 @Override
190 public void run() {
191 ServerSocket ss = null;
192 boolean alreadyStarted = false;
193 synchronized (lock) {
194 ss = this.ss;
195 if (!started && ss != null) {
196 started = true;
197 } else {
198 alreadyStarted = started;
199 }
200 }
201
202 if (alreadyStarted) {
203 tracer.error(name + ": cannot start server on port " + port
204 + ", it is already started");
205 return;
206 }
207
208 if (ss == null) {
209 tracer.error(name + ": cannot start server on port " + port
210 + ", it has already been used");
211 return;
212 }
213
214 try {
215 tracer.trace(name + ": server starting on port " + port + " ("
216 + (ssl ? "SSL" : "plain text") + ")");
217
218 while (started && !exiting) {
219 count(1);
220 final Socket s = ss.accept();
221 new Thread(new Runnable() {
222 @Override
223 public void run() {
224 ConnectActionServer action = null;
225 try {
226 action = createConnectActionServer(s);
227 action.connect();
228 } finally {
229 count(-1);
230 if (action != null) {
231 bytesReceived += action.getBytesReceived();
232 bytesSent += action.getBytesSent();
233 }
234 }
235 }
236 }).start();
237 }
238
239 // Will be covered by @link{Server#stop(long)} for timeouts
240 while (counter > 0) {
241 Thread.sleep(10);
242 }
243 } catch (Exception e) {
244 if (counter > 0) {
245 onError(e);
246 }
247 } finally {
248 try {
249 ss.close();
250 } catch (Exception e) {
251 onError(e);
252 }
253
254 this.ss = null;
255
256 started = false;
257 exiting = false;
258 counter = 0;
259
260 tracer.trace(name + ": client terminated on port " + port);
261 }
262 }
263
264 /**
265 * Will stop the server, synchronously and without a timeout.
266 */
267 public void stop() {
268 tracer.trace(name + ": stopping server");
269 stop(0, true);
270 }
271
272 /**
273 * Stop the server.
274 *
275 * @param timeout
276 * the maximum timeout to wait for existing actions to complete,
277 * or 0 for "no timeout"
278 * @param wait
279 * wait for the server to be stopped before returning
280 * (synchronous) or not (asynchronous)
281 */
282 public void stop(final long timeout, final boolean wait) {
283 if (wait) {
284 stop(timeout);
285 } else {
286 new Thread(new Runnable() {
287 @Override
288 public void run() {
289 stop(timeout);
290 }
291 }).start();
292 }
293 }
294
295 /**
296 * Stop the server (synchronous).
297 *
298 * @param timeout
299 * the maximum timeout to wait for existing actions to complete,
300 * or 0 for "no timeout"
301 */
302 private void stop(long timeout) {
303 tracer.trace(name + ": server stopping on port " + port);
304 synchronized (lock) {
305 if (started && !exiting) {
306 exiting = true;
307
308 try {
309 new ConnectActionClientObject(createSocket(null, port, ssl))
310 .connect();
311 long time = 0;
312 while (ss != null && timeout > 0 && timeout > time) {
313 Thread.sleep(10);
314 time += 10;
315 }
316 } catch (Exception e) {
317 if (ss != null) {
318 counter = 0; // will stop the main thread
319 onError(e);
320 }
321 }
322 }
323 }
324
325 // return only when stopped
326 while (started || exiting) {
327 try {
328 Thread.sleep(10);
329 } catch (InterruptedException e) {
330 }
331 }
332 }
333
334 /**
335 * Change the number of currently serviced actions.
336 *
337 * @param change
338 * the number to increase or decrease
339 *
340 * @return the current number after this operation
341 */
342 private int count(int change) {
343 synchronized (counterLock) {
344 counter += change;
345 return counter;
346 }
347 }
348
349 /**
350 * This method will be called on errors.
351 * <p>
352 * By default, it will only call the trace handler (so you may want to call
353 * super {@link Server#onError} if you override it).
354 *
355 * @param e
356 * the error
357 */
358 protected void onError(Exception e) {
359 tracer.error(e);
360 }
361
362 /**
363 * Create a {@link Socket}.
364 *
365 * @param host
366 * the host to connect to
367 * @param port
368 * the port to connect to
369 * @param ssl
370 * TRUE for SSL mode (or FALSE for plain text mode)
371 *
372 * @return the {@link Socket}
373 *
374 * @throws IOException
375 * in case of I/O error
376 * @throws UnknownHostException
377 * if the host is not known
378 * @throws IllegalArgumentException
379 * if the port parameter is outside the specified range of valid
380 * port values, which is between 0 and 65535, inclusive
381 */
382 static Socket createSocket(String host, int port, boolean ssl)
383 throws IOException {
384 Socket s;
385 if (ssl) {
386 s = SSLSocketFactory.getDefault().createSocket(host, port);
387 if (s instanceof SSLSocket) {
388 // Should always be the case
389 ((SSLSocket) s).setEnabledCipherSuites(ANON_CIPHERS);
390 }
391 } else {
392 s = new Socket(host, port);
393 }
394
395 return s;
396 }
397
398 /**
399 * Create a {@link ServerSocket}.
400 *
401 * @param port
402 * the port to accept connections on
403 * @param ssl
404 * TRUE for SSL mode (or FALSE for plain text mode)
405 *
406 * @return the {@link ServerSocket}
407 *
408 * @throws IOException
409 * in case of I/O error
410 * @throws UnknownHostException
411 * if the IP address of the host could not be determined
412 * @throws IllegalArgumentException
413 * if the port parameter is outside the specified range of valid
414 * port values, which is between 0 and 65535, inclusive
415 */
416 static ServerSocket createSocketServer(int port, boolean ssl)
417 throws IOException {
418 ServerSocket ss;
419 if (ssl) {
420 ss = SSLServerSocketFactory.getDefault().createServerSocket(port);
421 if (ss instanceof SSLServerSocket) {
422 // Should always be the case
423 ((SSLServerSocket) ss).setEnabledCipherSuites(ANON_CIPHERS);
424 }
425 } else {
426 ss = new ServerSocket(port);
427 }
428
429 return ss;
430 }
431
432 /**
433 * Return all the supported ciphers that do not use authentication.
434 *
435 * @return the list of such supported ciphers
436 */
437 public static String[] getAnonCiphers() {
438 List<String> anonCiphers = new ArrayList<String>();
439 for (String cipher : ((SSLSocketFactory) SSLSocketFactory.getDefault())
440 .getSupportedCipherSuites()) {
441 if (cipher.contains("_anon_")) {
442 anonCiphers.add(cipher);
443 }
444 }
445
446 return anonCiphers.toArray(new String[] {});
447 }
448 }