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