Add DCAE MOD design tool project
[dcaegen2/platform.git] / mod / designtool / designtool-web / src / main / java / org / apache / nifi / web / server / JettyServer.java
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  * Modifications to the original nifi code for the ONAP project are made
18  * available under the Apache License, Version 2.0
19  */
20 package org.apache.nifi.web.server;
21
22 import com.google.common.base.Strings;
23 import com.google.common.collect.Lists;
24 import java.io.BufferedReader;
25 import java.io.File;
26 import java.io.FileFilter;
27 import java.io.IOException;
28 import java.io.InputStreamReader;
29 import java.net.InetAddress;
30 import java.net.NetworkInterface;
31 import java.net.SocketException;
32 import java.net.URI;
33 import java.nio.file.Paths;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.EnumSet;
39 import java.util.Enumeration;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.Set;
46 import java.util.concurrent.TimeUnit;
47 import java.util.jar.JarEntry;
48 import java.util.jar.JarFile;
49 import java.util.stream.Collectors;
50 import javax.servlet.DispatcherType;
51 import javax.servlet.Filter;
52 import javax.servlet.ServletContext;
53 import org.apache.commons.collections4.CollectionUtils;
54 import org.apache.commons.lang3.StringUtils;
55 import org.apache.nifi.NiFiServer;
56 import org.apache.nifi.bundle.Bundle;
57 import org.apache.nifi.bundle.BundleDetails;
58 import org.apache.nifi.controller.UninheritableFlowException;
59 import org.apache.nifi.controller.serialization.FlowSerializationException;
60 import org.apache.nifi.controller.serialization.FlowSynchronizationException;
61 import org.apache.nifi.documentation.DocGenerator;
62 import org.apache.nifi.lifecycle.LifeCycleStartException;
63 import org.apache.nifi.nar.ExtensionDiscoveringManager;
64 import org.apache.nifi.nar.ExtensionManagerHolder;
65 import org.apache.nifi.nar.ExtensionMapping;
66 import org.apache.nifi.nar.ExtensionUiLoader;
67 import org.apache.nifi.nar.NarAutoLoader;
68 import org.apache.nifi.nar.DCAEAutoLoader;
69 import org.apache.nifi.nar.NarClassLoadersHolder;
70 import org.apache.nifi.nar.NarLoader;
71 import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
72 import org.apache.nifi.nar.StandardNarLoader;
73 import org.apache.nifi.processor.DataUnit;
74 import org.apache.nifi.security.util.KeyStoreUtils;
75 import org.apache.nifi.services.FlowService;
76 import org.apache.nifi.ui.extension.UiExtension;
77 import org.apache.nifi.ui.extension.UiExtensionMapping;
78 import org.apache.nifi.util.FormatUtils;
79 import org.apache.nifi.util.NiFiProperties;
80 import org.apache.nifi.web.ContentAccess;
81 import org.apache.nifi.web.NiFiWebConfigurationContext;
82 import org.apache.nifi.web.UiExtensionType;
83 import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
84 import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
85 import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
86 import org.apache.nifi.web.security.headers.XSSProtectionFilter;
87 import org.eclipse.jetty.annotations.AnnotationConfiguration;
88 import org.eclipse.jetty.deploy.App;
89 import org.eclipse.jetty.deploy.DeploymentManager;
90 import org.eclipse.jetty.server.Connector;
91 import org.eclipse.jetty.server.Handler;
92 import org.eclipse.jetty.server.HttpConfiguration;
93 import org.eclipse.jetty.server.HttpConnectionFactory;
94 import org.eclipse.jetty.server.SecureRequestCustomizer;
95 import org.eclipse.jetty.server.Server;
96 import org.eclipse.jetty.server.ServerConnector;
97 import org.eclipse.jetty.server.SslConnectionFactory;
98 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
99 import org.eclipse.jetty.server.handler.HandlerCollection;
100 import org.eclipse.jetty.server.handler.HandlerList;
101 import org.eclipse.jetty.server.handler.gzip.GzipHandler;
102 import org.eclipse.jetty.servlet.DefaultServlet;
103 import org.eclipse.jetty.servlet.FilterHolder;
104 import org.eclipse.jetty.servlet.ServletHolder;
105 import org.eclipse.jetty.util.ssl.SslContextFactory;
106 import org.eclipse.jetty.util.thread.QueuedThreadPool;
107 import org.eclipse.jetty.webapp.Configuration;
108 import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
109 import org.eclipse.jetty.webapp.WebAppClassLoader;
110 import org.eclipse.jetty.webapp.WebAppContext;
111 import org.slf4j.Logger;
112 import org.slf4j.LoggerFactory;
113 import org.springframework.beans.BeansException;
114 import org.springframework.context.ApplicationContext;
115 import org.springframework.web.context.WebApplicationContext;
116 import org.springframework.web.context.support.WebApplicationContextUtils;
117
118 /**
119  * Encapsulates the Jetty instance.
120  */
121 public class JettyServer implements NiFiServer, ExtensionUiLoader {
122
123     private static final Logger logger = LoggerFactory.getLogger(JettyServer.class);
124     private static final String WEB_DEFAULTS_XML = "org/apache/nifi/web/webdefault.xml";
125
126     private static final String CONTAINER_INCLUDE_PATTERN_KEY = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
127     private static final String CONTAINER_INCLUDE_PATTERN_VALUE = ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$";
128
129     private static final FileFilter WAR_FILTER = new FileFilter() {
130         @Override
131         public boolean accept(File pathname) {
132             final String nameToTest = pathname.getName().toLowerCase();
133             return nameToTest.endsWith(".war") && pathname.isFile();
134         }
135     };
136
137     private final Server server;
138     private final NiFiProperties props;
139
140     private Bundle systemBundle;
141     private Set<Bundle> bundles;
142     private ExtensionMapping extensionMapping;
143     private NarAutoLoader narAutoLoader;
144     private DCAEAutoLoader dcaeAutoLoader;
145
146     private WebAppContext webApiContext;
147     private WebAppContext webDocsContext;
148
149     // content viewer and mime type specific extensions
150     private WebAppContext webContentViewerContext;
151     private Collection<WebAppContext> contentViewerWebContexts;
152
153     // component (processor, controller service, reporting task) ui extensions
154     private UiExtensionMapping componentUiExtensions;
155     private Collection<WebAppContext> componentUiExtensionWebContexts;
156
157     private DeploymentManager deploymentManager;
158
159     public JettyServer(final NiFiProperties props, final Set<Bundle> bundles) {
160         final QueuedThreadPool threadPool = new QueuedThreadPool(props.getWebThreads());
161         threadPool.setName("NiFi Web Server");
162
163         // create the server
164         this.server = new Server(threadPool);
165         this.props = props;
166
167         // enable the annotation based configuration to ensure the jsp container is initialized properly
168         final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
169         classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName());
170
171         // configure server
172         configureConnectors(server);
173
174         // load wars from the bundle
175         final Handler warHandlers = loadInitialWars(bundles);
176
177         final HandlerList allHandlers = new HandlerList();
178
179         // Only restrict the host header if running in HTTPS mode
180         if (props.isHTTPSConfigured()) {
181             // Create a handler for the host header and add it to the server
182             HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
183             logger.info("Created HostHeaderHandler [" + hostHeaderHandler.toString() + "]");
184
185             // Add this before the WAR handlers
186             allHandlers.addHandler(hostHeaderHandler);
187         } else {
188             logger.info("Running in HTTP mode; host headers not restricted");
189         }
190
191
192         final ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
193         contextHandlers.addHandler(warHandlers);
194         allHandlers.addHandler(contextHandlers);
195         server.setHandler(allHandlers);
196
197         deploymentManager = new DeploymentManager();
198         deploymentManager.setContextAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
199         deploymentManager.setContexts(contextHandlers);
200         server.addBean(deploymentManager);
201     }
202
203     /**
204      * Instantiates this object but does not perform any configuration. Used for unit testing.
205      */
206      JettyServer(Server server, NiFiProperties properties) {
207         this.server = server;
208         this.props = properties;
209     }
210
211     private Handler loadInitialWars(final Set<Bundle> bundles) {
212
213         // load WARs
214         final Map<File, Bundle> warToBundleLookup = findWars(bundles);
215
216         // locate each war being deployed
217         File webUiWar = null;
218         File webApiWar = null;
219         File webErrorWar = null;
220         File webDocsWar = null;
221         File webContentViewerWar = null;
222         Map<File, Bundle> otherWars = new HashMap<>();
223         for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
224             final File war = warBundleEntry.getKey();
225             final Bundle warBundle = warBundleEntry.getValue();
226
227             if (war.getName().toLowerCase().startsWith("nifi-web-api")) {
228                 webApiWar = war;
229             } else if (war.getName().toLowerCase().startsWith("nifi-web-error")) {
230                 webErrorWar = war;
231             } else if (war.getName().toLowerCase().startsWith("nifi-web-docs")) {
232                 webDocsWar = war;
233             } else if (war.getName().toLowerCase().startsWith("nifi-web-content-viewer")) {
234                 webContentViewerWar = war;
235             } else if (war.getName().toLowerCase().startsWith("nifi-web")) {
236                 webUiWar = war;
237             } else {
238                 otherWars.put(war, warBundle);
239             }
240         }
241
242         // ensure the required wars were found
243         if (webUiWar == null) {
244             throw new RuntimeException("Unable to load nifi-web WAR");
245         } else if (webApiWar == null) {
246             throw new RuntimeException("Unable to load nifi-web-api WAR");
247         } else if (webDocsWar == null) {
248             throw new RuntimeException("Unable to load nifi-web-docs WAR");
249         } else if (webErrorWar == null) {
250             throw new RuntimeException("Unable to load nifi-web-error WAR");
251         } else if (webContentViewerWar == null) {
252             throw new RuntimeException("Unable to load nifi-web-content-viewer WAR");
253         }
254
255         // handlers for each war and init params for the web api
256         final ExtensionUiInfo extensionUiInfo = loadWars(otherWars);
257         componentUiExtensionWebContexts = new ArrayList<>(extensionUiInfo.getComponentUiExtensionWebContexts());
258         contentViewerWebContexts = new ArrayList<>(extensionUiInfo.getContentViewerWebContexts());
259         componentUiExtensions = new UiExtensionMapping(extensionUiInfo.getComponentUiExtensionsByType());
260
261         final HandlerCollection webAppContextHandlers = new HandlerCollection();
262         final Collection<WebAppContext> extensionUiContexts = extensionUiInfo.getWebAppContexts();
263         extensionUiContexts.stream().forEach(c -> webAppContextHandlers.addHandler(c));
264
265         final ClassLoader frameworkClassLoader = getClass().getClassLoader();
266
267         // load the web ui app
268         final WebAppContext webUiContext = loadWar(webUiWar, "/nifi", frameworkClassLoader);
269         webUiContext.getInitParams().put("oidc-supported", String.valueOf(props.isOidcEnabled()));
270         webUiContext.getInitParams().put("knox-supported", String.valueOf(props.isKnoxSsoEnabled()));
271         webUiContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths());
272         webAppContextHandlers.addHandler(webUiContext);
273
274         // load the web api app
275         webApiContext = loadWar(webApiWar, "/nifi-api", frameworkClassLoader);
276         webAppContextHandlers.addHandler(webApiContext);
277
278         // load the content viewer app
279         webContentViewerContext = loadWar(webContentViewerWar, "/nifi-content-viewer", frameworkClassLoader);
280         webContentViewerContext.getInitParams().putAll(extensionUiInfo.getMimeMappings());
281         webAppContextHandlers.addHandler(webContentViewerContext);
282
283         // create a web app for the docs
284         final String docsContextPath = "/nifi-docs";
285
286         // load the documentation war
287         webDocsContext = loadWar(webDocsWar, docsContextPath, frameworkClassLoader);
288
289         // add the servlets which serve the HTML documentation within the documentation web app
290         addDocsServlets(webDocsContext);
291
292         webAppContextHandlers.addHandler(webDocsContext);
293
294         // load the web error app
295         final WebAppContext webErrorContext = loadWar(webErrorWar, "/", frameworkClassLoader);
296         webErrorContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths());
297         webAppContextHandlers.addHandler(webErrorContext);
298
299         // deploy the web apps
300         return gzip(webAppContextHandlers);
301     }
302
303     @Override
304     public void loadExtensionUis(final Set<Bundle> bundles) {
305          // Find and load any WARs contained within the set of bundles...
306         final Map<File, Bundle> warToBundleLookup = findWars(bundles);
307         final ExtensionUiInfo extensionUiInfo = loadWars(warToBundleLookup);
308
309         final Collection<WebAppContext> webAppContexts = extensionUiInfo.getWebAppContexts();
310         if (CollectionUtils.isEmpty(webAppContexts)) {
311             logger.debug("No webapp contexts were loaded, returning...");
312             return;
313         }
314
315         // Deploy each WAR that was loaded...
316         for (final WebAppContext webAppContext : webAppContexts) {
317             final App extensionUiApp = new App(deploymentManager, null, "nifi-jetty-server", webAppContext);
318             deploymentManager.addApp(extensionUiApp);
319         }
320
321         final Collection<WebAppContext> componentUiExtensionWebContexts = extensionUiInfo.getComponentUiExtensionWebContexts();
322         final Collection<WebAppContext> contentViewerWebContexts = extensionUiInfo.getContentViewerWebContexts();
323
324         // Inject the configuration context and security filter into contexts that need it
325         final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
326         final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
327         final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
328         final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
329
330         performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
331         performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
332
333         // Merge results of current loading into previously loaded results...
334         this.componentUiExtensionWebContexts.addAll(componentUiExtensionWebContexts);
335         this.contentViewerWebContexts.addAll(contentViewerWebContexts);
336         this.componentUiExtensions.addUiExtensions(extensionUiInfo.getComponentUiExtensionsByType());
337
338         for (final WebAppContext webAppContext : webAppContexts) {
339             final Throwable t = webAppContext.getUnavailableException();
340             if (t != null) {
341                 logger.error("Unable to start context due to " + t.getMessage(), t);
342             }
343         }
344     }
345
346     private ExtensionUiInfo loadWars(final Map<File, Bundle> warToBundleLookup) {
347          // handlers for each war and init params for the web api
348         final List<WebAppContext> webAppContexts = new ArrayList<>();
349         final Map<String, String> mimeMappings = new HashMap<>();
350         final Collection<WebAppContext> componentUiExtensionWebContexts = new ArrayList<>();
351         final Collection<WebAppContext> contentViewerWebContexts = new ArrayList<>();
352         final Map<String, List<UiExtension>> componentUiExtensionsByType = new HashMap<>();
353
354         final ClassLoader frameworkClassLoader = getClass().getClassLoader();
355         final ClassLoader jettyClassLoader = frameworkClassLoader.getParent();
356
357         // deploy the other wars
358         if (!warToBundleLookup.isEmpty()) {
359             // ui extension organized by component type
360             for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
361                 final File war = warBundleEntry.getKey();
362                 final Bundle warBundle = warBundleEntry.getValue();
363
364                 // identify all known extension types in the war
365                 final Map<UiExtensionType, List<String>> uiExtensionInWar = new HashMap<>();
366                 identifyUiExtensionsForComponents(uiExtensionInWar, war);
367
368                 // only include wars that are for custom processor ui's
369                 if (!uiExtensionInWar.isEmpty()) {
370                     // get the context path
371                     String warName = StringUtils.substringBeforeLast(war.getName(), ".");
372                     String warContextPath = String.format("/%s", warName);
373
374                     // get the classloader for this war
375                     ClassLoader narClassLoaderForWar = warBundle.getClassLoader();
376
377                     // this should never be null
378                     if (narClassLoaderForWar == null) {
379                         narClassLoaderForWar = jettyClassLoader;
380                     }
381
382                     // create the extension web app context
383                     WebAppContext extensionUiContext = loadWar(war, warContextPath, narClassLoaderForWar);
384
385                     // create the ui extensions
386                     for (final Map.Entry<UiExtensionType, List<String>> entry : uiExtensionInWar.entrySet()) {
387                         final UiExtensionType extensionType = entry.getKey();
388                         final List<String> types = entry.getValue();
389
390                         if (UiExtensionType.ContentViewer.equals(extensionType)) {
391                             // consider each content type identified
392                             for (final String contentType : types) {
393                                 // map the content type to the context path
394                                 mimeMappings.put(contentType, warContextPath);
395                             }
396
397                             // this ui extension provides a content viewer
398                             contentViewerWebContexts.add(extensionUiContext);
399                         } else {
400                             // consider each component type identified
401                             for (final String componentTypeCoordinates : types) {
402                                 logger.info(String.format("Loading UI extension [%s, %s] for %s", extensionType, warContextPath, componentTypeCoordinates));
403
404                                 // record the extension definition
405                                 final UiExtension uiExtension = new UiExtension(extensionType, warContextPath);
406
407                                 // create if this is the first extension for this component type
408                                 List<UiExtension> componentUiExtensionsForType = componentUiExtensionsByType.get(componentTypeCoordinates);
409                                 if (componentUiExtensionsForType == null) {
410                                     componentUiExtensionsForType = new ArrayList<>();
411                                     componentUiExtensionsByType.put(componentTypeCoordinates, componentUiExtensionsForType);
412                                 }
413
414                                 // see if there is already a ui extension of this same time
415                                 if (containsUiExtensionType(componentUiExtensionsForType, extensionType)) {
416                                     throw new IllegalStateException(String.format("Encountered duplicate UI for %s", componentTypeCoordinates));
417                                 }
418
419                                 // record this extension
420                                 componentUiExtensionsForType.add(uiExtension);
421                             }
422
423                             // this ui extension provides a component custom ui
424                             componentUiExtensionWebContexts.add(extensionUiContext);
425                         }
426                     }
427
428                     // include custom ui web context in the handlers
429                     webAppContexts.add(extensionUiContext);
430                 }
431             }
432         }
433
434         return new ExtensionUiInfo(webAppContexts, mimeMappings, componentUiExtensionWebContexts, contentViewerWebContexts, componentUiExtensionsByType);
435     }
436
437     /**
438      * Returns whether or not the specified ui extensions already contains an extension of the specified type.
439      *
440      * @param componentUiExtensionsForType ui extensions for the type
441      * @param extensionType                type of ui extension
442      * @return whether or not the specified ui extensions already contains an extension of the specified type
443      */
444     private boolean containsUiExtensionType(final List<UiExtension> componentUiExtensionsForType, final UiExtensionType extensionType) {
445         for (final UiExtension uiExtension : componentUiExtensionsForType) {
446             if (extensionType.equals(uiExtension.getExtensionType())) {
447                 return true;
448             }
449         }
450
451         return false;
452     }
453
454     /**
455      * Enables compression for the specified handler.
456      *
457      * @param handler handler to enable compression for
458      * @return compression enabled handler
459      */
460     private Handler gzip(final Handler handler) {
461         final GzipHandler gzip = new GzipHandler();
462         gzip.setIncludedMethods("GET", "POST", "PUT", "DELETE");
463         gzip.setHandler(handler);
464         return gzip;
465     }
466
467     private Map<File, Bundle> findWars(final Set<Bundle> bundles) {
468         final Map<File, Bundle> wars = new HashMap<>();
469
470         // consider each nar working directory
471         bundles.forEach(bundle -> {
472             final BundleDetails details = bundle.getBundleDetails();
473             final File narDependencies = new File(details.getWorkingDirectory(), "NAR-INF/bundled-dependencies");
474             if (narDependencies.isDirectory()) {
475                 // list the wars from this nar
476                 final File[] narDependencyDirs = narDependencies.listFiles(WAR_FILTER);
477                 if (narDependencyDirs == null) {
478                     throw new IllegalStateException(String.format("Unable to access working directory for NAR dependencies in: %s", narDependencies.getAbsolutePath()));
479                 }
480
481                 // add each war
482                 for (final File war : narDependencyDirs) {
483                     wars.put(war, bundle);
484                 }
485             }
486         });
487
488         return wars;
489     }
490
491     private void readUiExtensions(final Map<UiExtensionType, List<String>> uiExtensions, final UiExtensionType uiExtensionType, final JarFile jarFile, final JarEntry jarEntry) throws IOException {
492         if (jarEntry == null) {
493             return;
494         }
495
496         // get an input stream for the nifi-processor configuration file
497         try (BufferedReader in = new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry)))) {
498
499             // read in each configured type
500             String rawComponentType;
501             while ((rawComponentType = in.readLine()) != null) {
502                 // extract the component type
503                 final String componentType = extractComponentType(rawComponentType);
504                 if (componentType != null) {
505                     List<String> extensions = uiExtensions.get(uiExtensionType);
506
507                     // if there are currently no extensions for this type create it
508                     if (extensions == null) {
509                         extensions = new ArrayList<>();
510                         uiExtensions.put(uiExtensionType, extensions);
511                     }
512
513                     // add the specified type
514                     extensions.add(componentType);
515                 }
516             }
517         }
518     }
519
520     /**
521      * Identifies all known UI extensions and stores them in the specified map.
522      *
523      * @param uiExtensions extensions
524      * @param warFile      war
525      */
526     private void identifyUiExtensionsForComponents(final Map<UiExtensionType, List<String>> uiExtensions, final File warFile) {
527         try (final JarFile jarFile = new JarFile(warFile)) {
528             // locate the ui extensions
529             readUiExtensions(uiExtensions, UiExtensionType.ContentViewer, jarFile, jarFile.getJarEntry("META-INF/nifi-content-viewer"));
530             readUiExtensions(uiExtensions, UiExtensionType.ProcessorConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-processor-configuration"));
531             readUiExtensions(uiExtensions, UiExtensionType.ControllerServiceConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-controller-service-configuration"));
532             readUiExtensions(uiExtensions, UiExtensionType.ReportingTaskConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-reporting-task-configuration"));
533         } catch (IOException ioe) {
534             logger.warn(String.format("Unable to inspect %s for a UI extensions.", warFile));
535         }
536     }
537
538     /**
539      * Extracts the component type. Trims the line and considers comments.
540      * Returns null if no type was found.
541      *
542      * @param line line
543      * @return type
544      */
545     private String extractComponentType(final String line) {
546         final String trimmedLine = line.trim();
547         if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) {
548             final int indexOfPound = trimmedLine.indexOf("#");
549             return (indexOfPound > 0) ? trimmedLine.substring(0, indexOfPound) : trimmedLine;
550         }
551         return null;
552     }
553
554     private WebAppContext loadWar(final File warFile, final String contextPath, final ClassLoader parentClassLoader) {
555         final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath);
556         webappContext.setContextPath(contextPath);
557         webappContext.setDisplayName(contextPath);
558
559         // instruction jetty to examine these jars for tlds, web-fragments, etc
560         webappContext.setAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
561
562         // remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib
563         List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses()));
564         serverClasses.remove("org.slf4j.");
565         webappContext.setServerClasses(serverClasses.toArray(new String[0]));
566         webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML);
567
568         // get the temp directory for this webapp
569         File tempDir = new File(props.getWebWorkingDirectory(), warFile.getName());
570         if (tempDir.exists() && !tempDir.isDirectory()) {
571             throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory");
572         } else if (!tempDir.exists()) {
573             final boolean made = tempDir.mkdirs();
574             if (!made) {
575                 throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created");
576             }
577         }
578         if (!(tempDir.canRead() && tempDir.canWrite())) {
579             throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege");
580         }
581
582         // configure the temp dir
583         webappContext.setTempDirectory(tempDir);
584
585         // configure the max form size (3x the default)
586         webappContext.setMaxFormContentSize(600000);
587
588         // add HTTP security headers to all responses
589         final String ALL_PATHS = "/*";
590         ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class));
591         if(props.isHTTPSConfigured()) {
592             filters.add(StrictTransportSecurityFilter.class);
593         }
594         filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext));
595
596         try {
597             // configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ...
598             webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext));
599         } catch (final IOException ioe) {
600             startUpFailure(ioe);
601         }
602
603         logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
604         return webappContext;
605     }
606
607     private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) {
608         FilterHolder holder = new FilterHolder(clazz);
609         holder.setName(clazz.getSimpleName());
610         webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class));
611     }
612
613     private void addDocsServlets(WebAppContext docsContext) {
614         try {
615             // Load the nifi/docs directory
616             final File docsDir = getDocsDir("docs");
617
618             // load the component documentation working directory
619             final File componentDocsDirPath = props.getComponentDocumentationWorkingDirectory();
620             final File workingDocsDirectory = getWorkingDocsDirectory(componentDocsDirPath);
621
622             // Load the API docs
623             final File webApiDocsDir = getWebApiDocsDir();
624
625             // Create the servlet which will serve the static resources
626             ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class);
627             defaultHolder.setInitParameter("dirAllowed", "false");
628
629             ServletHolder docs = new ServletHolder("docs", DefaultServlet.class);
630             docs.setInitParameter("resourceBase", docsDir.getPath());
631
632             ServletHolder components = new ServletHolder("components", DefaultServlet.class);
633             components.setInitParameter("resourceBase", workingDocsDirectory.getPath());
634
635             ServletHolder restApi = new ServletHolder("rest-api", DefaultServlet.class);
636             restApi.setInitParameter("resourceBase", webApiDocsDir.getPath());
637
638             docsContext.addServlet(docs, "/html/*");
639             docsContext.addServlet(components, "/components/*");
640             docsContext.addServlet(restApi, "/rest-api/*");
641
642             docsContext.addServlet(defaultHolder, "/");
643
644             logger.info("Loading documents web app with context path set to " + docsContext.getContextPath());
645
646         } catch (Exception ex) {
647             logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage());
648             startUpFailure(ex);
649         }
650     }
651
652
653     /**
654      * Returns a File object for the directory containing NIFI documentation.
655      * <p>
656      * Formerly, if the docsDirectory did not exist NIFI would fail to start
657      * with an IllegalStateException and a rather unhelpful log message.
658      * NIFI-2184 updates the process such that if the docsDirectory does not
659      * exist an attempt will be made to create the directory. If that is
660      * successful NIFI will no longer fail and will start successfully barring
661      * any other errors. The side effect of the docsDirectory not being present
662      * is that the documentation links under the 'General' portion of the help
663      * page will not be accessible, but at least the process will be running.
664      *
665      * @param docsDirectory Name of documentation directory in installation directory.
666      * @return A File object to the documentation directory; else startUpFailure called.
667      */
668     private File getDocsDir(final String docsDirectory) {
669         File docsDir;
670         try {
671             docsDir = Paths.get(docsDirectory).toRealPath().toFile();
672         } catch (IOException ex) {
673             logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable.");
674             docsDir = new File(docsDirectory).getAbsoluteFile();
675             final boolean made = docsDir.mkdirs();
676             if (!made) {
677                 logger.error("Failed to create 'docs' directory!");
678                 startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created"));
679             }
680         }
681         return docsDir;
682     }
683
684     private File getWorkingDocsDirectory(final File componentDocsDirPath) {
685         File workingDocsDirectory = null;
686         try {
687             workingDocsDirectory = componentDocsDirPath.toPath().toRealPath().getParent().toFile();
688         } catch (IOException ex) {
689             logger.error("Failed to load :" + componentDocsDirPath.getAbsolutePath());
690             startUpFailure(ex);
691         }
692         return workingDocsDirectory;
693     }
694
695     private File getWebApiDocsDir() {
696         // load the rest documentation
697         final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
698         if (!webApiDocsDir.exists()) {
699             final boolean made = webApiDocsDir.mkdirs();
700             if (!made) {
701                 logger.error("Failed to create " + webApiDocsDir.getAbsolutePath());
702                 startUpFailure(new IOException(webApiDocsDir.getAbsolutePath() + " could not be created"));
703             }
704         }
705         return webApiDocsDir;
706     }
707
708     private void configureConnectors(final Server server) throws ServerConfigurationException {
709         // create the http configuration
710         final HttpConfiguration httpConfiguration = new HttpConfiguration();
711         final int headerSize = DataUnit.parseDataSize(props.getWebMaxHeaderSize(), DataUnit.B).intValue();
712         httpConfiguration.setRequestHeaderSize(headerSize);
713         httpConfiguration.setResponseHeaderSize(headerSize);
714
715         // Check if both HTTP and HTTPS connectors are configured and fail if both are configured
716         if (bothHttpAndHttpsConnectorsConfigured(props)) {
717             logger.error("NiFi only supports one mode of HTTP or HTTPS operation, not both simultaneously. " +
718                     "Check the nifi.properties file and ensure that either the HTTP hostname and port or the HTTPS hostname and port are empty");
719             startUpFailure(new IllegalStateException("Only one of the HTTP and HTTPS connectors can be configured at one time"));
720         }
721
722         if (props.getSslPort() != null) {
723             configureHttpsConnector(server, httpConfiguration);
724         } else if (props.getPort() != null) {
725             configureHttpConnector(server, httpConfiguration);
726         } else {
727             logger.error("Neither the HTTP nor HTTPS connector was configured in nifi.properties");
728             startUpFailure(new IllegalStateException("Must configure HTTP or HTTPS connector"));
729         }
730     }
731
732     /**
733      * Configures an HTTPS connector and adds it to the server.
734      *
735      * @param server            the Jetty server instance
736      * @param httpConfiguration the configuration object for the HTTPS protocol settings
737      */
738     private void configureHttpsConnector(Server server, HttpConfiguration httpConfiguration) {
739         String hostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
740         final Integer port = props.getSslPort();
741         String connectorLabel = "HTTPS";
742         final Map<String, String> httpsNetworkInterfaces = props.getHttpsNetworkInterfaces();
743         ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port);
744
745         configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc);
746     }
747
748     /**
749      * Configures an HTTP connector and adds it to the server.
750      *
751      * @param server            the Jetty server instance
752      * @param httpConfiguration the configuration object for the HTTP protocol settings
753      */
754     private void configureHttpConnector(Server server, HttpConfiguration httpConfiguration) {
755         String hostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
756         final Integer port = props.getPort();
757         String connectorLabel = "HTTP";
758         final Map<String, String> httpNetworkInterfaces = props.getHttpNetworkInterfaces();
759         ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> new ServerConnector(s, new HttpConnectionFactory(c));
760
761         configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpNetworkInterfaces, scc);
762     }
763
764     /**
765      * Configures an HTTP(S) connector for the server given the provided parameters. The functionality between HTTP and HTTPS connectors is largely similar.
766      * Here the common behavior has been extracted into a shared method and the respective calling methods obtain the right values and a lambda function for the differing behavior.
767      *
768      * @param server                 the Jetty server instance
769      * @param configuration          the HTTP/HTTPS configuration instance
770      * @param hostname               the hostname from the nifi.properties file
771      * @param port                   the port to expose
772      * @param connectorLabel         used for log output (e.g. "HTTP" or "HTTPS")
773      * @param networkInterfaces      the map of network interfaces from nifi.properties
774      * @param serverConnectorCreator a function which accepts a {@code Server} and {@code HttpConnection} instance and returns a {@code ServerConnector}
775      */
776     private void configureGenericConnector(Server server, HttpConfiguration configuration, String hostname, Integer port, String connectorLabel, Map<String, String> networkInterfaces,
777                                            ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> serverConnectorCreator) {
778         if (port < 0 || (int) Math.pow(2, 16) <= port) {
779             throw new ServerConfigurationException("Invalid " + connectorLabel + " port: " + port);
780         }
781
782         logger.info("Configuring Jetty for " + connectorLabel + " on port: " + port);
783
784         final List<Connector> serverConnectors = Lists.newArrayList();
785
786         // Calculate Idle Timeout as twice the auto-refresh interval. This ensures that even with some variance in timing,
787         // we are able to avoid closing connections from users' browsers most of the time. This can make a significant difference
788         // in HTTPS connections, as each HTTPS connection that is established must perform the SSL handshake.
789         final String autoRefreshInterval = props.getAutoRefreshInterval();
790         final long autoRefreshMillis = autoRefreshInterval == null ? 30000L : FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS);
791         final long idleTimeout = autoRefreshMillis * 2;
792
793         // If the interfaces collection is empty or each element is empty
794         if (networkInterfaces.isEmpty() || networkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) {
795             final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
796
797             // Set host and port
798             if (StringUtils.isNotBlank(hostname)) {
799                 serverConnector.setHost(hostname);
800             }
801             serverConnector.setPort(port);
802             serverConnector.setIdleTimeout(idleTimeout);
803             serverConnectors.add(serverConnector);
804         } else {
805             // Add connectors for all IPs from network interfaces
806             serverConnectors.addAll(Lists.newArrayList(networkInterfaces.values().stream().map(ifaceName -> {
807                 NetworkInterface iface = null;
808                 try {
809                     iface = NetworkInterface.getByName(ifaceName);
810                 } catch (SocketException e) {
811                     logger.error("Unable to get network interface by name {}", ifaceName, e);
812                 }
813                 if (iface == null) {
814                     logger.warn("Unable to find network interface named {}", ifaceName);
815                 }
816                 return iface;
817             }).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream())
818                     .map(inetAddress -> {
819                         final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
820
821                         // Set host and port
822                         serverConnector.setHost(inetAddress.getHostAddress());
823                         serverConnector.setPort(port);
824                         serverConnector.setIdleTimeout(idleTimeout);
825
826                         return serverConnector;
827                     }).collect(Collectors.toList())));
828         }
829         // Add all connectors
830         serverConnectors.forEach(server::addConnector);
831     }
832
833     /**
834      * Returns true if there are configured properties for both HTTP and HTTPS connectors (specifically port because the hostname can be left blank in the HTTP connector).
835      * Prints a warning log message with the relevant properties.
836      *
837      * @param props the NiFiProperties
838      * @return true if both ports are present
839      */
840     static boolean bothHttpAndHttpsConnectorsConfigured(NiFiProperties props) {
841         Integer httpPort = props.getPort();
842         String httpHostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
843
844         Integer httpsPort = props.getSslPort();
845         String httpsHostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
846
847         if (httpPort != null && httpsPort != null) {
848             logger.warn("Both the HTTP and HTTPS connectors are configured in nifi.properties. Only one of these connectors should be configured. See the NiFi Admin Guide for more details");
849             logger.warn("HTTP connector:   http://" + httpHostname + ":" + httpPort);
850             logger.warn("HTTPS connector: https://" + httpsHostname + ":" + httpsPort);
851             return true;
852         }
853
854         return false;
855     }
856
857     private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration, int port) {
858         // add some secure config
859         final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
860         httpsConfiguration.setSecureScheme("https");
861         httpsConfiguration.setSecurePort(port);
862         httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
863
864         // build the connector
865         return new ServerConnector(server,
866                 new SslConnectionFactory(createSslContextFactory(), "http/1.1"),
867                 new HttpConnectionFactory(httpsConfiguration));
868     }
869
870     private SslContextFactory createSslContextFactory() {
871         final SslContextFactory contextFactory = new SslContextFactory();
872         configureSslContextFactory(contextFactory, props);
873         return contextFactory;
874     }
875
876     protected static void configureSslContextFactory(SslContextFactory contextFactory, NiFiProperties props) {
877         // require client auth when not supporting login, Kerberos service, or anonymous access
878         if (props.isClientAuthRequiredForRestApi()) {
879             contextFactory.setNeedClientAuth(true);
880         } else {
881             contextFactory.setWantClientAuth(true);
882         }
883
884         /* below code sets JSSE system properties when values are provided */
885         // keystore properties
886         if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_KEYSTORE))) {
887             contextFactory.setKeyStorePath(props.getProperty(NiFiProperties.SECURITY_KEYSTORE));
888         }
889         String keyStoreType = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE);
890         if (StringUtils.isNotBlank(keyStoreType)) {
891             contextFactory.setKeyStoreType(keyStoreType);
892             String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(keyStoreType);
893             if (StringUtils.isNoneEmpty(keyStoreProvider)) {
894                 contextFactory.setKeyStoreProvider(keyStoreProvider);
895             }
896         }
897         final String keystorePassword = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD);
898         final String keyPassword = props.getProperty(NiFiProperties.SECURITY_KEY_PASSWD);
899         if (StringUtils.isNotBlank(keystorePassword)) {
900             // if no key password was provided, then assume the keystore password is the same as the key password.
901             final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword;
902             contextFactory.setKeyStorePassword(keystorePassword);
903             contextFactory.setKeyManagerPassword(defaultKeyPassword);
904         } else if (StringUtils.isNotBlank(keyPassword)) {
905             // since no keystore password was provided, there will be no keystore integrity check
906             contextFactory.setKeyManagerPassword(keyPassword);
907         }
908
909         // truststore properties
910         if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE))) {
911             contextFactory.setTrustStorePath(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE));
912         }
913         String trustStoreType = props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE);
914         if (StringUtils.isNotBlank(trustStoreType)) {
915             contextFactory.setTrustStoreType(trustStoreType);
916             String trustStoreProvider = KeyStoreUtils.getKeyStoreProvider(trustStoreType);
917             if (StringUtils.isNoneEmpty(trustStoreProvider)) {
918                 contextFactory.setTrustStoreProvider(trustStoreProvider);
919             }
920         }
921         if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD))) {
922             contextFactory.setTrustStorePassword(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD));
923         }
924     }
925
926     @Override
927     public void start() {
928         try {
929             // Create a standard extension manager and discover extensions
930             final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager();
931             extensionManager.discoverExtensions(systemBundle, bundles);
932             extensionManager.logClassLoaderMapping();
933
934             // Set the extension manager into the holder which makes it available to the Spring context via a factory bean
935             ExtensionManagerHolder.init(extensionManager);
936
937             // Generate docs for extensions
938             DocGenerator.generate(props, extensionManager, extensionMapping);
939
940             // start the server
941             server.start();
942
943             // ensure everything started successfully
944             for (Handler handler : server.getChildHandlers()) {
945                 // see if the handler is a web app
946                 if (handler instanceof WebAppContext) {
947                     WebAppContext context = (WebAppContext) handler;
948
949                     // see if this webapp had any exceptions that would
950                     // cause it to be unavailable
951                     if (context.getUnavailableException() != null) {
952                         startUpFailure(context.getUnavailableException());
953                     }
954                 }
955             }
956
957             // ensure the appropriate wars deployed successfully before injecting the NiFi context and security filters
958             // this must be done after starting the server (and ensuring there were no start up failures)
959             if (webApiContext != null) {
960                 // give the web api the component ui extensions
961                 final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
962                 webApiServletContext.setAttribute("nifi-ui-extensions", componentUiExtensions);
963
964                 // get the application context
965                 final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
966                 final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
967                 final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
968
969                 // component ui extensions
970                 performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
971
972                 // content viewer extensions
973                 performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
974
975                 // content viewer controller
976                 if (webContentViewerContext != null) {
977                     final ContentAccess contentAccess = webApplicationContext.getBean("contentAccess", ContentAccess.class);
978
979                     // add the content access
980                     final ServletContext webContentViewerServletContext = webContentViewerContext.getServletHandler().getServletContext();
981                     webContentViewerServletContext.setAttribute("nifi-content-access", contentAccess);
982
983                     if (securityFilter != null) {
984                         webContentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
985                     }
986                 }
987             }
988
989             // ensure the web document war was loaded and provide the extension mapping
990             if (webDocsContext != null) {
991                 final ServletContext webDocsServletContext = webDocsContext.getServletHandler().getServletContext();
992                 webDocsServletContext.setAttribute("nifi-extension-mapping", extensionMapping);
993             }
994
995             // if this nifi is a node in a cluster, start the flow service and load the flow - the
996             // flow service is loaded here for clustered nodes because the loading of the flow will
997             // initialize the connection between the node and the NCM. if the node connects (starts
998             // heartbeating, etc), the NCM may issue web requests before the application (wars) have
999             // finished loading. this results in the node being disconnected since its unable to
1000             // successfully respond to the requests. to resolve this, flow loading was moved to here
1001             // (after the wars have been successfully deployed) when this nifi instance is a node
1002             // in a cluster
1003             if (props.isNode()) {
1004
1005                 FlowService flowService = null;
1006                 try {
1007
1008                     logger.info("Loading Flow...");
1009
1010                     ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(webApiContext.getServletContext());
1011                     flowService = ctx.getBean("flowService", FlowService.class);
1012
1013                     // start and load the flow
1014                     flowService.start();
1015                     flowService.load(null);
1016
1017                     logger.info("Flow loaded successfully.");
1018
1019                 } catch (BeansException | LifeCycleStartException | IOException | FlowSerializationException | FlowSynchronizationException | UninheritableFlowException e) {
1020                     // ensure the flow service is terminated
1021                     if (flowService != null && flowService.isRunning()) {
1022                         flowService.stop(false);
1023                     }
1024                     logger.error("Unable to load flow due to: " + e, e);
1025                     throw new Exception("Unable to load flow due to: " + e); // cannot wrap the exception as they are not defined in a classloader accessible to the caller
1026                 }
1027             }
1028
1029             final NarLoader narLoader = new StandardNarLoader(
1030                     props.getExtensionsWorkingDirectory(),
1031                     props.getComponentDocumentationWorkingDirectory(),
1032                     NarClassLoadersHolder.getInstance(),
1033                     extensionManager,
1034                     extensionMapping,
1035                     this);
1036
1037             narAutoLoader = new NarAutoLoader(props.getNarAutoLoadDirectory(), narLoader);
1038             narAutoLoader.start();
1039
1040             URI jarsIndex = props.getDCAEJarIndexURI();
1041
1042             // REVIEW: Added ability to turn off the loaidng of dcae jars by providing no url
1043             if (jarsIndex == null) {
1044                 StringBuilder sb = new StringBuilder();
1045                 sb.append("Auto-loading of DCAE jars is turned off.");
1046                 sb.append(" You must set the value of \"nifi.dcae.jars.index.url\"");
1047                 sb.append(" to the full url to the index JSON of DCAE jars in the nifi.properties file");
1048                 sb.append(" in order to activate this feature.");
1049                 logger.warn(sb.toString());
1050             } else {
1051                 this.dcaeAutoLoader = new DCAEAutoLoader();
1052                 this.dcaeAutoLoader.start(jarsIndex, extensionManager);
1053             }
1054
1055             // dump the application url after confirming everything started successfully
1056             dumpUrls();
1057         } catch (Exception ex) {
1058             startUpFailure(ex);
1059         }
1060     }
1061
1062     private void performInjectionForComponentUis(final Collection<WebAppContext> componentUiExtensionWebContexts,
1063                                                  final NiFiWebConfigurationContext configurationContext, final FilterHolder securityFilter) {
1064         if (CollectionUtils.isNotEmpty(componentUiExtensionWebContexts)) {
1065             for (final WebAppContext customUiContext : componentUiExtensionWebContexts) {
1066                 // set the NiFi context in each custom ui servlet context
1067                 final ServletContext customUiServletContext = customUiContext.getServletHandler().getServletContext();
1068                 customUiServletContext.setAttribute("nifi-web-configuration-context", configurationContext);
1069
1070                 // add the security filter to any ui extensions wars
1071                 if (securityFilter != null) {
1072                     customUiContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
1073                 }
1074             }
1075         }
1076     }
1077
1078     private void performInjectionForContentViewerUis(final Collection<WebAppContext> contentViewerWebContexts,
1079                                                      final FilterHolder securityFilter) {
1080         if (CollectionUtils.isNotEmpty(contentViewerWebContexts)) {
1081             for (final WebAppContext contentViewerContext : contentViewerWebContexts) {
1082                 // add the security filter to any content viewer  wars
1083                 if (securityFilter != null) {
1084                     contentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
1085                 }
1086             }
1087         }
1088     }
1089
1090     private void dumpUrls() throws SocketException {
1091         final List<String> urls = new ArrayList<>();
1092
1093         for (Connector connector : server.getConnectors()) {
1094             if (connector instanceof ServerConnector) {
1095                 final ServerConnector serverConnector = (ServerConnector) connector;
1096
1097                 Set<String> hosts = new HashSet<>();
1098
1099                 // determine the hosts
1100                 if (StringUtils.isNotBlank(serverConnector.getHost())) {
1101                     hosts.add(serverConnector.getHost());
1102                 } else {
1103                     Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
1104                     if (networkInterfaces != null) {
1105                         for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
1106                             for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
1107                                 hosts.add(inetAddress.getHostAddress());
1108                             }
1109                         }
1110                     }
1111                 }
1112
1113                 // ensure some hosts were found
1114                 if (!hosts.isEmpty()) {
1115                     String scheme = "http";
1116                     if (props.getSslPort() != null && serverConnector.getPort() == props.getSslPort()) {
1117                         scheme = "https";
1118                     }
1119
1120                     // dump each url
1121                     for (String host : hosts) {
1122                         urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort()));
1123                     }
1124                 }
1125             }
1126         }
1127
1128         if (urls.isEmpty()) {
1129             logger.warn("NiFi has started, but the UI is not available on any hosts. Please verify the host properties.");
1130         } else {
1131             // log the ui location
1132             logger.info("NiFi has started. The UI is available at the following URLs:");
1133             for (final String url : urls) {
1134                 logger.info(String.format("%s/nifi", url));
1135             }
1136         }
1137     }
1138
1139     private void startUpFailure(Throwable t) {
1140         System.err.println("Failed to start web server: " + t.getMessage());
1141         System.err.println("Shutting down...");
1142         logger.warn("Failed to start web server... shutting down.", t);
1143         System.exit(1);
1144     }
1145
1146     @Override
1147     public void setExtensionMapping(ExtensionMapping extensionMapping) {
1148         this.extensionMapping = extensionMapping;
1149     }
1150
1151     @Override
1152     public void setBundles(Bundle systemBundle, Set<Bundle> bundles) {
1153         this.systemBundle = systemBundle;
1154         this.bundles = bundles;
1155     }
1156
1157     @Override
1158     public void stop() {
1159         try {
1160             server.stop();
1161         } catch (Exception ex) {
1162             logger.warn("Failed to stop web server", ex);
1163         }
1164
1165         try {
1166             if (narAutoLoader != null) {
1167                 narAutoLoader.stop();
1168             }
1169
1170             if (dcaeAutoLoader != null) {
1171                 dcaeAutoLoader.stop();
1172             }
1173         } catch (Exception e) {
1174             logger.warn("Failed to stop NAR auto-loader", e);
1175         }
1176     }
1177
1178     /**
1179      * Holds the result of loading WARs for custom UIs.
1180      */
1181     private static class ExtensionUiInfo {
1182
1183         private final Collection<WebAppContext> webAppContexts;
1184         private final Map<String, String> mimeMappings;
1185         private final Collection<WebAppContext> componentUiExtensionWebContexts;
1186         private final Collection<WebAppContext> contentViewerWebContexts;
1187         private final Map<String, List<UiExtension>> componentUiExtensionsByType;
1188
1189         public ExtensionUiInfo(final Collection<WebAppContext> webAppContexts,
1190                                final Map<String, String> mimeMappings,
1191                                final Collection<WebAppContext> componentUiExtensionWebContexts,
1192                                final Collection<WebAppContext> contentViewerWebContexts,
1193                                final Map<String, List<UiExtension>> componentUiExtensionsByType) {
1194             this.webAppContexts = webAppContexts;
1195             this.mimeMappings = mimeMappings;
1196             this.componentUiExtensionWebContexts = componentUiExtensionWebContexts;
1197             this.contentViewerWebContexts = contentViewerWebContexts;
1198             this.componentUiExtensionsByType = componentUiExtensionsByType;
1199         }
1200
1201         public Collection<WebAppContext> getWebAppContexts() {
1202             return webAppContexts;
1203         }
1204
1205         public Map<String, String> getMimeMappings() {
1206             return mimeMappings;
1207         }
1208
1209         public Collection<WebAppContext> getComponentUiExtensionWebContexts() {
1210             return componentUiExtensionWebContexts;
1211         }
1212
1213         public Collection<WebAppContext> getContentViewerWebContexts() {
1214             return contentViewerWebContexts;
1215         }
1216
1217         public Map<String, List<UiExtension>> getComponentUiExtensionsByType() {
1218             return componentUiExtensionsByType;
1219         }
1220     }
1221 }
1222
1223 @FunctionalInterface
1224 interface ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> {
1225     ServerConnector create(Server server, HttpConfiguration httpConfiguration);
1226 }