2 * ============LICENSE_START=======================================================
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
13 * http://www.apache.org/licenses/LICENSE-2.0
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.
21 * SPDX-License-Identifier: Apache-2.0
22 * ============LICENSE_END=========================================================
25 package org.onap.policy.common.endpoints.http.server.internal;
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;
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;
58 * Http Server implementation using Embedded Jetty.
61 public abstract class JettyServletServer implements HttpServletServer, Runnable {
64 * Keystore/Truststore system property names.
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
74 private static final Logger logger = LoggerFactory.getLogger(JettyServletServer.class);
76 private static final String NOT_SUPPORTED = " is not supported on this type of jetty server";
82 protected final String name;
85 * Server host address.
88 protected final String host;
91 * Server port to bind.
94 protected final int port;
97 * Should SNI host checking be done.
100 protected boolean sniHostCheck;
103 * Server auth username.
106 protected String user;
109 * Server auth password name.
112 protected String password;
115 * Server base context path.
117 protected final String contextPath;
120 * Embedded jetty server.
122 protected final Server jettyServer;
127 protected final ServletContextHandler context;
132 protected final ServerConnector connector;
137 protected Thread jettyThread;
140 * Container for default servlets.
142 protected final Map<String, ServletHolder> servlets = new HashMap<>();
148 protected final Object startCondition = new Object();
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
159 * @throws IllegalArgumentException if invalid parameters are passed in
161 protected JettyServletServer(String name, boolean https, String host, int port, boolean sniHostCheck,
162 String contextPath) {
163 String srvName = name;
165 if (srvName == null || srvName.isEmpty()) {
166 srvName = "http-" + port;
169 if (port <= 0 || port >= 65535) {
170 throw new IllegalArgumentException("Invalid Port provided: " + port);
173 String srvHost = host;
174 if (srvHost == null || srvHost.isEmpty()) {
175 srvHost = "localhost";
178 String ctxtPath = contextPath;
179 if (ctxtPath == null || ctxtPath.isEmpty()) {
187 this.sniHostCheck = sniHostCheck;
189 this.contextPath = ctxtPath;
191 this.context = new ServletContextHandler(ServletContextHandler.SESSIONS);
192 this.context.setContextPath(ctxtPath);
194 this.jettyServer = new Server();
196 var requestLog = new CustomRequestLog(new Slf4jRequestLogWriter(), CustomRequestLog.EXTENDED_NCSA_FORMAT);
197 this.jettyServer.setRequestLog(requestLog);
200 this.connector = httpsConnector();
202 this.connector = httpConnector();
205 this.connector.setName(srvName);
206 this.connector.setReuseAddress(true);
207 this.connector.setPort(port);
208 this.connector.setHost(srvHost);
210 this.jettyServer.addConnector(this.connector);
211 this.jettyServer.setHandler(context);
214 protected JettyServletServer(String name, String host, int port, boolean sniHostCheck, String contextPath) {
215 this(name, false, host, port, sniHostCheck, contextPath);
219 public void addFilterClass(String filterPath, String filterClass) {
220 if (filterClass == null || filterClass.isEmpty()) {
221 throw new IllegalArgumentException("No filter class provided");
224 String tempFilterPath = filterPath;
225 if (filterPath == null || filterPath.isEmpty()) {
226 tempFilterPath = "/*";
229 context.addFilter(filterClass, tempFilterPath,
230 EnumSet.of(jakarta.servlet.DispatcherType.INCLUDE, jakarta.servlet.DispatcherType.REQUEST));
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));
239 protected ServletHolder getServlet(String servletClass, String servletPath) {
240 synchronized (servlets) {
241 return servlets.computeIfAbsent(servletPath, key -> context.addServlet(servletClass, servletPath));
246 * Returns the https connector.
248 * @return the server connector
250 public ServerConnector httpsConnector() {
251 SslContextFactory.Server sslContextFactoryServer = new SslContextFactory.Server();
253 String keyStore = System.getProperty(SYSTEM_KEYSTORE_PROPERTY_NAME);
254 if (keyStore != null) {
255 sslContextFactoryServer.setKeyStorePath(keyStore);
257 String ksPassword = System.getProperty(SYSTEM_KEYSTORE_PASSWORD_PROPERTY_NAME);
258 if (ksPassword != null) {
259 sslContextFactoryServer.setKeyStorePassword(ksPassword);
263 String trustStore = System.getProperty(SYSTEM_TRUSTSTORE_PROPERTY_NAME);
264 if (trustStore != null) {
265 sslContextFactoryServer.setTrustStorePath(trustStore);
267 String tsPassword = System.getProperty(SYSTEM_TRUSTSTORE_PASSWORD_PROPERTY_NAME);
268 if (tsPassword != null) {
269 sslContextFactoryServer.setTrustStorePassword(tsPassword);
274 var httpsConfiguration = new HttpConfiguration();
275 SecureRequestCustomizer src = new SecureRequestCustomizer();
276 src.setSniHostCheck(sniHostCheck);
277 httpsConfiguration.addCustomizer(src);
279 return new ServerConnector(jettyServer, sslContextFactoryServer, new HttpConnectionFactory(httpsConfiguration));
282 public ServerConnector httpConnector() {
283 return new ServerConnector(this.jettyServer);
287 public void setBasicAuthentication(String user, String password, String servletPath) {
288 String srvltPath = servletPath;
290 if (user == null || user.isEmpty() || password == null || password.isEmpty()) {
291 throw new IllegalArgumentException("Missing user and/or password");
294 if (srvltPath == null || srvltPath.isEmpty()) {
298 final var hashLoginService = new HashLoginService();
299 final var userStore = new UserStore();
300 userStore.addUser(user, Credential.getCredential(password), new String[] {
303 hashLoginService.setUserStore(userStore);
304 hashLoginService.setName(this.connector.getName() + "-login-service");
306 var constraint = new Constraint();
307 constraint.setName(Constraint.__BASIC_AUTH);
308 constraint.setRoles(new String[] {
311 constraint.setAuthenticate(true);
313 var constraintMapping = new ConstraintMapping();
314 constraintMapping.setConstraint(constraint);
315 constraintMapping.setPathSpec(srvltPath);
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);
323 this.context.setSecurityHandler(securityHandler);
326 this.password = password;
330 * jetty server execution.
335 logger.info("{}: STARTING", this);
337 this.jettyServer.start();
339 if (logger.isTraceEnabled()) {
340 logger.trace("{}: STARTED: {}", this, this.jettyServer.dump());
343 synchronized (this.startCondition) {
344 this.startCondition.notifyAll();
347 this.jettyServer.join();
349 } catch (InterruptedException e) {
350 logger.error("{}: error found while bringing up server", this, e);
351 Thread.currentThread().interrupt();
353 } catch (Exception e) {
354 logger.error("{}: error found while bringing up server", this, e);
359 public boolean waitedStart(long maxWaitTime) throws InterruptedException {
360 logger.info("{}: WAITED-START", this);
362 if (maxWaitTime < 0) {
363 throw new IllegalArgumentException("max-wait-time cannot be negative");
366 long pendingWaitTime = maxWaitTime;
372 synchronized (this.startCondition) {
374 while (!this.jettyServer.isRunning()) {
376 long startTs = System.currentTimeMillis();
378 this.startCondition.wait(pendingWaitTime);
380 if (maxWaitTime == 0) {
381 /* spurious notification */
385 long endTs = System.currentTimeMillis();
386 pendingWaitTime = pendingWaitTime - (endTs - startTs);
388 logger.info("{}: pending time is {} ms.", this, pendingWaitTime);
390 if (pendingWaitTime <= 0) {
394 } catch (InterruptedException e) {
395 logger.warn("{}: waited-start has been interrupted", this);
400 return this.jettyServer.isRunning();
405 public boolean start() {
406 logger.info("{}: STARTING", this);
408 synchronized (this) {
409 if (jettyThread == null || !this.jettyThread.isAlive()) {
411 this.jettyThread = new Thread(this);
412 this.jettyThread.setName(this.name + "-" + this.port);
413 this.jettyThread.start();
421 public boolean stop() {
422 logger.info("{}: STOPPING", this);
424 synchronized (this) {
425 if (jettyThread == null) {
429 if (!jettyThread.isAlive()) {
430 this.jettyThread = null;
434 this.connector.stop();
435 } catch (Exception e) {
436 logger.error("{}: error while stopping management server", this, e);
440 this.jettyServer.stop();
441 } catch (Exception e) {
442 logger.error("{}: error while stopping management server", this, e);
453 public void shutdown() {
454 logger.info("{}: SHUTTING DOWN", this);
458 Thread jettyThreadCopy;
459 synchronized (this) {
460 if ((jettyThreadCopy = this.jettyThread) == null) {
465 if (jettyThreadCopy.isAlive()) {
467 jettyThreadCopy.join(2000L);
468 } catch (InterruptedException e) {
469 logger.warn("{}: error while shutting down management server", this);
470 Thread.currentThread().interrupt();
472 if (!jettyThreadCopy.isInterrupted()) {
474 jettyThreadCopy.interrupt();
475 } catch (Exception e) {
477 logger.warn("{}: exception while shutting down (OK)", this, e);
482 this.jettyServer.destroy();
486 public boolean isAlive() {
487 if (this.jettyThread != null) {
488 return this.jettyThread.isAlive();
495 public void setSerializationProvider(String provider) {
496 throw new UnsupportedOperationException("setSerializationProvider()" + NOT_SUPPORTED);
500 public void addServletClass(String servletPath, String servletClass) {
501 throw new UnsupportedOperationException("addServletClass()" + NOT_SUPPORTED);
505 public void addStdServletClass(@NonNull String servletPath, @NonNull String plainServletClass) {
506 this.getServlet(plainServletClass, servletPath);
510 public void setPrometheus(String metricsPath) {
511 this.getServlet(PrometheusMetricsServlet.class, metricsPath);
512 JvmMetrics.builder().register();
516 public boolean isPrometheus() {
517 for (ServletHolder servlet : context.getServletHandler().getServlets()) {
518 if (PrometheusMetricsServlet.class.getName().equals(servlet.getClassName())) {
526 public void addServletPackage(String servletPath, String restPackage) {
527 throw new UnsupportedOperationException("addServletPackage()" + NOT_SUPPORTED);
531 public void addServletResource(String servletPath, String resourceBase) {
532 throw new UnsupportedOperationException("addServletResource()" + NOT_SUPPORTED);