2 * ============LICENSE_START=======================================================
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
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.
20 * ============LICENSE_END=========================================================
23 package org.onap.policy.common.endpoints.http.server.internal;
25 import io.prometheus.client.exporter.MetricsServlet;
26 import io.prometheus.client.hotspot.DefaultExports;
27 import java.util.EnumSet;
28 import java.util.HashMap;
30 import javax.servlet.DispatcherType;
31 import javax.servlet.Servlet;
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;
59 * Http Server implementation using Embedded Jetty.
62 public abstract class JettyServletServer implements HttpServletServer, Runnable {
65 * Keystore/Truststore system property names.
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
75 private static Logger logger = LoggerFactory.getLogger(JettyServletServer.class);
77 private static final String NOT_SUPPORTED = " is not supported on this type of jetty server";
83 protected final String name;
86 * Server host address.
89 protected final String host;
92 * Server port to bind.
95 protected final int port;
98 * Server auth user name.
101 protected String user;
104 * Server auth password name.
107 protected String password;
110 * Server base context path.
112 protected final String contextPath;
115 * Embedded jetty server.
117 protected final Server jettyServer;
122 protected final ServletContextHandler context;
127 protected final ServerConnector connector;
132 protected Thread jettyThread;
135 * Container for default servlets.
137 protected final Map<String, ServletHolder> servlets = new HashMap<>();
143 protected Object startCondition = new Object();
148 * @param name server name
149 * @param host server host
150 * @param port server port
151 * @param contextPath context path
153 * @throws IllegalArgumentException if invalid parameters are passed in
155 protected JettyServletServer(String name, boolean https, String host, int port, String contextPath) {
156 String srvName = name;
158 if (srvName == null || srvName.isEmpty()) {
159 srvName = "http-" + port;
162 if (port <= 0 || port >= 65535) {
163 throw new IllegalArgumentException("Invalid Port provided: " + port);
166 String srvHost = host;
167 if (srvHost == null || srvHost.isEmpty()) {
168 srvHost = "localhost";
171 String ctxtPath = contextPath;
172 if (ctxtPath == null || ctxtPath.isEmpty()) {
181 this.contextPath = ctxtPath;
183 this.context = new ServletContextHandler(ServletContextHandler.SESSIONS);
184 this.context.setContextPath(ctxtPath);
186 this.jettyServer = new Server();
188 var requestLog = new CustomRequestLog(new Slf4jRequestLogWriter(), CustomRequestLog.EXTENDED_NCSA_FORMAT);
189 this.jettyServer.setRequestLog(requestLog);
192 this.connector = httpsConnector();
194 this.connector = httpConnector();
197 this.connector.setName(srvName);
198 this.connector.setReuseAddress(true);
199 this.connector.setPort(port);
200 this.connector.setHost(srvHost);
202 this.jettyServer.addConnector(this.connector);
203 this.jettyServer.setHandler(context);
206 protected JettyServletServer(String name, String host, int port, String contextPath) {
207 this(name, false, host, port, contextPath);
211 public void addFilterClass(String filterPath, String filterClass) {
212 if (filterClass == null || filterClass.isEmpty()) {
213 throw new IllegalArgumentException("No filter class provided");
216 String tempFilterPath = filterPath;
217 if (filterPath == null || filterPath.isEmpty()) {
218 tempFilterPath = "/*";
221 context.addFilter(filterClass, tempFilterPath, EnumSet.of(DispatcherType.INCLUDE, DispatcherType.REQUEST));
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));
230 protected ServletHolder getServlet(String servletClass, String servletPath) {
231 synchronized (servlets) {
232 return servlets.computeIfAbsent(servletPath, key -> context.addServlet(servletClass, servletPath));
237 * Returns the https connector.
239 * @return the server connector
241 public ServerConnector httpsConnector() {
242 SslContextFactory.Server sslContextFactoryServer = new SslContextFactory.Server();
244 String keyStore = System.getProperty(SYSTEM_KEYSTORE_PROPERTY_NAME);
245 if (keyStore != null) {
246 sslContextFactoryServer.setKeyStorePath(keyStore);
248 String ksPassword = System.getProperty(SYSTEM_KEYSTORE_PASSWORD_PROPERTY_NAME);
249 if (ksPassword != null) {
250 sslContextFactoryServer.setKeyStorePassword(ksPassword);
254 String trustStore = System.getProperty(SYSTEM_TRUSTSTORE_PROPERTY_NAME);
255 if (trustStore != null) {
256 sslContextFactoryServer.setTrustStorePath(trustStore);
258 String tsPassword = System.getProperty(SYSTEM_TRUSTSTORE_PASSWORD_PROPERTY_NAME);
259 if (tsPassword != null) {
260 sslContextFactoryServer.setTrustStorePassword(tsPassword);
264 var https = new HttpConfiguration();
265 https.addCustomizer(new SecureRequestCustomizer());
267 return new ServerConnector(jettyServer, sslContextFactoryServer, new HttpConnectionFactory(https));
270 public ServerConnector httpConnector() {
271 return new ServerConnector(this.jettyServer);
275 public void setAafAuthentication(String filterPath) {
276 this.addFilterClass(filterPath, CadiFilter.class.getName());
280 public boolean isAaf() {
281 for (FilterHolder filter : context.getServletHandler().getFilters()) {
282 if (CadiFilter.class.getName().equals(filter.getClassName())) {
290 public void setBasicAuthentication(String user, String password, String servletPath) {
291 String srvltPath = servletPath;
293 if (user == null || user.isEmpty() || password == null || password.isEmpty()) {
294 throw new IllegalArgumentException("Missing user and/or password");
297 if (srvltPath == null || srvltPath.isEmpty()) {
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");
307 var constraint = new Constraint();
308 constraint.setName(Constraint.__BASIC_AUTH);
309 constraint.setRoles(new String[] {"user"});
310 constraint.setAuthenticate(true);
312 var constraintMapping = new ConstraintMapping();
313 constraintMapping.setConstraint(constraint);
314 constraintMapping.setPathSpec(srvltPath);
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);
322 this.context.setSecurityHandler(securityHandler);
325 this.password = password;
329 * jetty server execution.
334 logger.info("{}: STARTING", this);
336 this.jettyServer.start();
338 if (logger.isTraceEnabled()) {
339 logger.trace("{}: STARTED: {}", this, this.jettyServer.dump());
342 synchronized (this.startCondition) {
343 this.startCondition.notifyAll();
346 this.jettyServer.join();
348 } catch (InterruptedException e) {
349 logger.error("{}: error found while bringing up server", this, e);
350 Thread.currentThread().interrupt();
352 } catch (Exception e) {
353 logger.error("{}: error found while bringing up server", this, e);
358 public boolean waitedStart(long maxWaitTime) throws InterruptedException {
359 logger.info("{}: WAITED-START", this);
361 if (maxWaitTime < 0) {
362 throw new IllegalArgumentException("max-wait-time cannot be negative");
365 long pendingWaitTime = maxWaitTime;
371 synchronized (this.startCondition) {
373 while (!this.jettyServer.isRunning()) {
375 long startTs = System.currentTimeMillis();
377 this.startCondition.wait(pendingWaitTime);
379 if (maxWaitTime == 0) {
380 /* spurious notification */
384 long endTs = System.currentTimeMillis();
385 pendingWaitTime = pendingWaitTime - (endTs - startTs);
387 logger.info("{}: pending time is {} ms.", this, pendingWaitTime);
389 if (pendingWaitTime <= 0) {
393 } catch (InterruptedException e) {
394 logger.warn("{}: waited-start has been interrupted", this);
399 return this.jettyServer.isRunning();
404 public boolean start() {
405 logger.info("{}: STARTING", this);
407 synchronized (this) {
408 if (jettyThread == null || !this.jettyThread.isAlive()) {
410 this.jettyThread = new Thread(this);
411 this.jettyThread.setName(this.name + "-" + this.port);
412 this.jettyThread.start();
420 public boolean stop() {
421 logger.info("{}: STOPPING", this);
423 synchronized (this) {
424 if (jettyThread == null) {
428 if (!jettyThread.isAlive()) {
429 this.jettyThread = null;
433 this.connector.stop();
434 } catch (Exception e) {
435 logger.error("{}: error while stopping management server", this, e);
439 this.jettyServer.stop();
440 } catch (Exception e) {
441 logger.error("{}: error while stopping management server", this, e);
452 public void shutdown() {
453 logger.info("{}: SHUTTING DOWN", this);
457 Thread jettyThreadCopy;
458 synchronized (this) {
459 if ((jettyThreadCopy = this.jettyThread) == null) {
464 if (jettyThreadCopy.isAlive()) {
466 jettyThreadCopy.join(2000L);
467 } catch (InterruptedException e) {
468 logger.warn("{}: error while shutting down management server", this);
469 Thread.currentThread().interrupt();
471 if (!jettyThreadCopy.isInterrupted()) {
473 jettyThreadCopy.interrupt();
474 } catch (Exception e) {
476 logger.warn("{}: exception while shutting down (OK)", this, e);
481 this.jettyServer.destroy();
485 public boolean isAlive() {
486 if (this.jettyThread != null) {
487 return this.jettyThread.isAlive();
494 public void setSerializationProvider(String provider) {
495 throw new UnsupportedOperationException("setSerializationProvider()" + NOT_SUPPORTED);
499 public void addServletClass(String servletPath, String servletClass) {
500 throw new UnsupportedOperationException("addServletClass()" + NOT_SUPPORTED);
504 public void addStdServletClass(@NonNull String servletPath, @NonNull String plainServletClass) {
505 this.getServlet(plainServletClass, servletPath);
509 public void setPrometheus(String metricsPath) {
510 this.getServlet(MetricsServlet.class, metricsPath);
511 DefaultExports.initialize();
515 public boolean isPrometheus() {
516 for (ServletHolder servlet : context.getServletHandler().getServlets()) {
517 if (MetricsServlet.class.getName().equals(servlet.getClassName())) {
525 public void addServletPackage(String servletPath, String restPackage) {
526 throw new UnsupportedOperationException("addServletPackage()" + NOT_SUPPORTED);
530 public void addServletResource(String servletPath, String resourceBase) {
531 throw new UnsupportedOperationException("addServletResource()" + NOT_SUPPORTED);