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