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