b866a81e8d2744585cae2e58dbd35dfacc0f8c7e
[policy/common.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP
4  * ================================================================================
5  * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.policy.common.endpoints.http.server.internal;
22
23 import com.fasterxml.jackson.annotation.JsonIgnore;
24
25 import java.util.EnumSet;
26 import javax.servlet.DispatcherType;
27 import org.eclipse.jetty.security.ConstraintMapping;
28 import org.eclipse.jetty.security.ConstraintSecurityHandler;
29 import org.eclipse.jetty.security.HashLoginService;
30 import org.eclipse.jetty.security.authentication.BasicAuthenticator;
31 import org.eclipse.jetty.server.HttpConfiguration;
32 import org.eclipse.jetty.server.HttpConnectionFactory;
33 import org.eclipse.jetty.server.SecureRequestCustomizer;
34 import org.eclipse.jetty.server.Server;
35 import org.eclipse.jetty.server.ServerConnector;
36 import org.eclipse.jetty.server.Slf4jRequestLog;
37 import org.eclipse.jetty.servlet.ServletContextHandler;
38 import org.eclipse.jetty.util.security.Constraint;
39 import org.eclipse.jetty.util.security.Credential;
40 import org.eclipse.jetty.util.ssl.SslContextFactory;
41 import org.onap.policy.common.endpoints.http.server.HttpServletServer;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * Http Server implementation using Embedded Jetty.
47  */
48 public abstract class JettyServletServer implements HttpServletServer, Runnable {
49
50     /**
51      * Keystore/Truststore system property names.
52      */
53     public static final String SYSTEM_KEYSTORE_PROPERTY_NAME = "javax.net.ssl.keyStore";
54     public static final String SYSTEM_KEYSTORE_PASSWORD_PROPERTY_NAME = "javax.net.ssl.keyStorePassword";
55     public static final String SYSTEM_TRUSTSTORE_PROPERTY_NAME = "javax.net.ssl.trustStore";
56     public static final String SYSTEM_TRUSTSTORE_PASSWORD_PROPERTY_NAME = "javax.net.ssl.trustStorePassword";
57
58     /**
59      * Logger.
60      */
61     private static Logger logger = LoggerFactory.getLogger(JettyServletServer.class);
62
63     /**
64      * Server name.
65      */
66     protected final String name;
67
68     /**
69      * Server host address.
70      */
71     protected final String host;
72
73     /**
74      * Server port to bind.
75      */
76     protected final int port;
77
78     /**
79      * Server auth user name.
80      */
81     protected String user;
82
83     /**
84      * Server auth password name.
85      */
86     protected String password;
87
88     /**
89      * Server base context path.
90      */
91     protected final String contextPath;
92
93     /**
94      * Embedded jetty server.
95      */
96     protected final Server jettyServer;
97
98     /**
99      * Servlet context.
100      */
101     protected final ServletContextHandler context;
102
103     /**
104      * Jetty connector.
105      */
106     protected final ServerConnector connector;
107
108     /**
109      * Jetty thread.
110      */
111     protected volatile Thread jettyThread;
112
113     /**
114      * Start condition.
115      */
116     protected Object startCondition = new Object();
117
118     /**
119      * Constructor.
120      * 
121      * @param name server name
122      * @param host server host
123      * @param port server port
124      * @param contextPath context path
125      * 
126      * @throws IllegalArgumentException if invalid parameters are passed in
127      */
128     public JettyServletServer(String name, boolean https, String host, int port, String contextPath) {
129         String srvName = name;
130         String srvHost = host;
131         String ctxtPath = contextPath;
132
133         if (srvName == null || srvName.isEmpty()) {
134             srvName = "http-" + port;
135         }
136
137         if (port <= 0 || port >= 65535) {
138             throw new IllegalArgumentException("Invalid Port provided: " + port);
139         }
140
141         if (srvHost == null || srvHost.isEmpty()) {
142             srvHost = "localhost";
143         }
144
145         if (ctxtPath == null || ctxtPath.isEmpty()) {
146             ctxtPath = "/";
147         }
148
149         this.name = srvName;
150
151         this.host = srvHost;
152         this.port = port;
153
154         this.contextPath = ctxtPath;
155
156         this.context = new ServletContextHandler(ServletContextHandler.SESSIONS);
157         this.context.setContextPath(ctxtPath);
158
159         this.jettyServer = new Server();
160         this.jettyServer.setRequestLog(new Slf4jRequestLog());
161
162         if (https) {
163             this.connector = httpsConnector();
164         } else {
165             this.connector = httpConnector();
166         }
167         
168         this.connector.setName(srvName);
169         this.connector.setReuseAddress(true);
170         this.connector.setPort(port);
171         this.connector.setHost(srvHost);
172
173         this.jettyServer.addConnector(this.connector);
174         this.jettyServer.setHandler(context);
175     }
176
177     public JettyServletServer(String name, String host, int port, String contextPath) {
178         this(name, false, host, port, contextPath);
179     }
180
181     @Override
182     public void addFilterClass(String aFilterPath, String aFilterClass) {
183         if (aFilterClass == null || aFilterClass.isEmpty()) {
184             throw new IllegalArgumentException("No filter class provided");
185         }
186
187         String filterPath = aFilterPath;
188         if (aFilterPath == null || aFilterPath.isEmpty()) {
189             filterPath = "/*";
190         }
191
192         context.addFilter(aFilterClass, filterPath,
193                 EnumSet.of(DispatcherType.INCLUDE, DispatcherType.REQUEST));
194     }
195
196     public ServerConnector httpsConnector() {
197         SslContextFactory sslContextFactory = new SslContextFactory();
198
199         String keyStore = System.getProperty(SYSTEM_KEYSTORE_PROPERTY_NAME);
200         if (keyStore != null) {
201             sslContextFactory.setKeyStorePath(keyStore);
202
203             String ksPassword = System.getProperty(SYSTEM_KEYSTORE_PASSWORD_PROPERTY_NAME);
204             if (ksPassword != null) {
205                 sslContextFactory.setKeyStorePassword(ksPassword);
206             }
207         }
208
209         String trustStore = System.getProperty(SYSTEM_TRUSTSTORE_PROPERTY_NAME);
210         if (trustStore != null) {
211             sslContextFactory.setTrustStorePath(trustStore);
212
213             String tsPassword = System.getProperty(SYSTEM_TRUSTSTORE_PASSWORD_PROPERTY_NAME);
214             if (tsPassword != null) {
215                 sslContextFactory.setTrustStorePassword(tsPassword);
216             }
217         }
218
219         HttpConfiguration https = new HttpConfiguration();
220         https.addCustomizer(new SecureRequestCustomizer());
221
222         return new ServerConnector(jettyServer, sslContextFactory, new HttpConnectionFactory(https));
223     }
224
225     public ServerConnector httpConnector() {
226         return new ServerConnector(this.jettyServer);
227     }
228
229     @Override
230     public void setBasicAuthentication(String user, String password, String servletPath) {
231         String srvltPath = servletPath;
232
233         if (user == null || user.isEmpty() || password == null || password.isEmpty()) {
234             throw new IllegalArgumentException("Missing user and/or password");
235         }
236
237         if (srvltPath == null || srvltPath.isEmpty()) {
238             srvltPath = "/*";
239         }
240
241         HashLoginService hashLoginService = new HashLoginService();
242         hashLoginService.putUser(user, Credential.getCredential(password), new String[] {"user"});
243         hashLoginService.setName(this.connector.getName() + "-login-service");
244
245         Constraint constraint = new Constraint();
246         constraint.setName(Constraint.__BASIC_AUTH);
247         constraint.setRoles(new String[] {"user"});
248         constraint.setAuthenticate(true);
249
250         ConstraintMapping constraintMapping = new ConstraintMapping();
251         constraintMapping.setConstraint(constraint);
252         constraintMapping.setPathSpec(srvltPath);
253
254         ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
255         securityHandler.setAuthenticator(new BasicAuthenticator());
256         securityHandler.setRealmName(this.connector.getName() + "-realm");
257         securityHandler.addConstraintMapping(constraintMapping);
258         securityHandler.setLoginService(hashLoginService);
259
260         this.context.setSecurityHandler(securityHandler);
261
262         this.user = user;
263         this.password = password;
264     }
265
266     /**
267      * jetty server execution.
268      */
269     @Override
270     public void run() {
271         try {
272             logger.info("{}: STARTING", this);
273
274             this.jettyServer.start();
275
276             if (logger.isInfoEnabled()) {
277                 logger.info("{}: STARTED: {}", this, this.jettyServer.dump());
278             }
279
280             synchronized (this.startCondition) {
281                 this.startCondition.notifyAll();
282             }
283
284             this.jettyServer.join();
285         } catch (Exception e) {
286             logger.error("{}: error found while bringing up server", this, e);
287         }
288     }
289
290     @Override
291     public boolean waitedStart(long maxWaitTime) throws InterruptedException {
292         logger.info("{}: WAITED-START", this);
293
294         if (maxWaitTime < 0) {
295             throw new IllegalArgumentException("max-wait-time cannot be negative");
296         }
297
298         long pendingWaitTime = maxWaitTime;
299
300         if (!this.start()) {
301             return false;
302         }
303
304         synchronized (this.startCondition) {
305
306             while (!this.jettyServer.isRunning()) {
307                 try {
308                     long startTs = System.currentTimeMillis();
309
310                     this.startCondition.wait(pendingWaitTime);
311
312                     if (maxWaitTime == 0) {
313                         /* spurious notification */
314                         continue;
315                     }
316
317                     long endTs = System.currentTimeMillis();
318                     pendingWaitTime = pendingWaitTime - (endTs - startTs);
319
320                     logger.info("{}: pending time is {} ms.", this, pendingWaitTime);
321
322                     if (pendingWaitTime <= 0) {
323                         return false;
324                     }
325
326                 } catch (InterruptedException e) {
327                     logger.warn("{}: waited-start has been interrupted", this);
328                     throw e;
329                 }
330             }
331
332             return this.jettyServer.isRunning();
333         }
334     }
335
336     @Override
337     public boolean start() {
338         logger.info("{}: STARTING", this);
339
340         synchronized (this) {
341             if (jettyThread == null || !this.jettyThread.isAlive()) {
342
343                 this.jettyThread = new Thread(this);
344                 this.jettyThread.setName(this.name + "-" + this.port);
345                 this.jettyThread.start();
346             }
347         }
348
349         return true;
350     }
351
352     @Override
353     public boolean stop() {
354         logger.info("{}: STOPPING", this);
355
356         synchronized (this) {
357             if (jettyThread == null) {
358                 return true;
359             }
360
361             if (!jettyThread.isAlive()) {
362                 this.jettyThread = null;
363             }
364
365             try {
366                 this.connector.stop();
367             } catch (Exception e) {
368                 logger.error("{}: error while stopping management server", this, e);
369             }
370
371             try {
372                 this.jettyServer.stop();
373             } catch (Exception e) {
374                 logger.error("{}: error while stopping management server", this, e);
375                 return false;
376             }
377
378             Thread.yield();
379         }
380
381         return true;
382     }
383
384     @Override
385     public void shutdown() {
386         logger.info("{}: SHUTTING DOWN", this);
387
388         this.stop();
389
390         if (this.jettyThread == null) {
391             return;
392         }
393
394         Thread jettyThreadCopy = this.jettyThread;
395
396         if (jettyThreadCopy.isAlive()) {
397             try {
398                 jettyThreadCopy.join(2000L);
399             } catch (InterruptedException e) {
400                 logger.warn("{}: error while shutting down management server", this);
401                 Thread.currentThread().interrupt();
402             }
403             if (!jettyThreadCopy.isInterrupted()) {
404                 try {
405                     jettyThreadCopy.interrupt();
406                 } catch (Exception e) {
407                     // do nothing
408                     logger.warn("{}: exception while shutting down (OK)", this, e);
409                 }
410             }
411         }
412
413         this.jettyServer.destroy();
414     }
415
416     @Override
417     public boolean isAlive() {
418         if (this.jettyThread != null) {
419             return this.jettyThread.isAlive();
420         }
421
422         return false;
423     }
424
425     @Override
426     public int getPort() {
427         return this.port;
428     }
429
430     /**
431      * Get name.
432      * 
433      * @return the name
434      */
435     public String getName() {
436         return name;
437     }
438
439     /**
440      * Get host.
441      * 
442      * @return the host
443      */
444     public String getHost() {
445         return host;
446     }
447
448     /**
449      * Get user.
450      * 
451      * @return the user
452      */
453     public String getUser() {
454         return user;
455     }
456
457     /**
458      * Get password.
459      * 
460      * @return the password
461      */
462     @JsonIgnore
463     public String getPassword() {
464         return password;
465     }
466
467     @Override
468     public String toString() {
469         StringBuilder builder = new StringBuilder();
470         builder.append("JettyServer [name=").append(name).append(", host=").append(host).append(", port=").append(port)
471                 .append(", user=").append(user).append(", password=").append(password != null).append(", contextPath=")
472                 .append(contextPath).append(", jettyServer=").append(jettyServer).append(", context=")
473                 .append(this.context).append(", connector=").append(connector).append(", jettyThread=")
474                 .append(jettyThread).append("]");
475         return builder.toString();
476     }
477
478 }