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