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