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