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