blob: c70f6b93cf92a13e1a3348287648c97ef5288f32 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.server;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.NiFiServer;
import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.bundle.BundleDetails;
import org.apache.nifi.controller.DecommissionTask;
import org.apache.nifi.controller.UninheritableFlowException;
import org.apache.nifi.controller.serialization.FlowSerializationException;
import org.apache.nifi.controller.serialization.FlowSynchronizationException;
import org.apache.nifi.diagnostics.DiagnosticsDump;
import org.apache.nifi.diagnostics.DiagnosticsDumpElement;
import org.apache.nifi.diagnostics.DiagnosticsFactory;
import org.apache.nifi.diagnostics.ThreadDumpTask;
import org.apache.nifi.documentation.DocGenerator;
import org.apache.nifi.lifecycle.LifeCycleStartException;
import org.apache.nifi.nar.ExtensionDiscoveringManager;
import org.apache.nifi.nar.ExtensionManagerHolder;
import org.apache.nifi.nar.ExtensionMapping;
import org.apache.nifi.nar.ExtensionUiLoader;
import org.apache.nifi.nar.NarAutoLoader;
import org.apache.nifi.nar.NarClassLoadersHolder;
import org.apache.nifi.nar.NarLoader;
import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
import org.apache.nifi.nar.StandardNarLoader;
import org.apache.nifi.processor.DataUnit;
import org.apache.nifi.security.util.KeyStoreUtils;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.services.FlowService;
import org.apache.nifi.ui.extension.UiExtension;
import org.apache.nifi.ui.extension.UiExtensionMapping;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.ContentAccess;
import org.apache.nifi.web.NiFiWebConfigurationContext;
import org.apache.nifi.web.UiExtensionType;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.apache.nifi.web.security.requests.ContentLengthFilter;
import org.apache.nifi.web.server.util.TrustStoreScanner;
import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.deploy.App;
import org.eclipse.jetty.deploy.DeploymentManager;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.DoSFilter;
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
/**
* Encapsulates the Jetty instance.
*/
public class JettyServer implements NiFiServer, ExtensionUiLoader {
private static final Logger logger = LoggerFactory.getLogger(JettyServer.class);
private static final String WEB_DEFAULTS_XML = "org/apache/nifi/web/webdefault.xml";
private static final String CONTAINER_INCLUDE_PATTERN_KEY = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
private static final String CONTAINER_INCLUDE_PATTERN_VALUE = ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$";
private static final String CONTEXT_PATH_ALL = "/*";
private static final String CONTEXT_PATH_ROOT = "/";
private static final String CONTEXT_PATH_NIFI = "/nifi";
private static final String CONTEXT_PATH_NIFI_API = "/nifi-api";
private static final String CONTEXT_PATH_NIFI_CONTENT_VIEWER = "/nifi-content-viewer";
private static final String CONTEXT_PATH_NIFI_DOCS = "/nifi-docs";
private static final String RELATIVE_PATH_ACCESS_TOKEN = "/access/token";
private static final int DOS_FILTER_REJECT_REQUEST = -1;
private static final FileFilter WAR_FILTER = pathname -> {
final String nameToTest = pathname.getName().toLowerCase();
return nameToTest.endsWith(".war") && pathname.isFile();
};
// property parsing util
private static final String REGEX_SPLIT_PROPERTY = ",\\s*";
protected static final String JOIN_ARRAY = ", ";
private Server server;
private NiFiProperties props;
private Bundle systemBundle;
private Set<Bundle> bundles;
private ExtensionMapping extensionMapping;
private NarAutoLoader narAutoLoader;
private DiagnosticsFactory diagnosticsFactory;
private SslContextFactory.Server sslContextFactory;
private DecommissionTask decommissionTask;
private WebAppContext webApiContext;
private WebAppContext webDocsContext;
// content viewer and mime type specific extensions
private WebAppContext webContentViewerContext;
private Collection<WebAppContext> contentViewerWebContexts;
// component (processor, controller service, reporting task) ui extensions
private UiExtensionMapping componentUiExtensions;
private Collection<WebAppContext> componentUiExtensionWebContexts;
private DeploymentManager deploymentManager;
/**
* Default no-arg constructor for ServiceLoader
*/
public JettyServer() {
}
public void init() {
final QueuedThreadPool threadPool = new QueuedThreadPool(props.getWebThreads());
threadPool.setName("NiFi Web Server");
// create the server
this.server = new Server(threadPool);
// enable the annotation based configuration to ensure the jsp container is initialized properly
final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName());
// configure server
configureConnectors(server);
// load wars from the bundle
final Handler warHandlers = loadInitialWars(bundles);
final HandlerList allHandlers = new HandlerList();
// Only restrict the host header if running in HTTPS mode
if (props.isHTTPSConfigured()) {
// Create a handler for the host header and add it to the server
final HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
logger.info("Created HostHeaderHandler [{}}]", hostHeaderHandler);
// Add this before the WAR handlers
allHandlers.addHandler(hostHeaderHandler);
} else {
logger.info("Running in HTTP mode; host headers not restricted");
}
final ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
contextHandlers.addHandler(warHandlers);
allHandlers.addHandler(contextHandlers);
server.setHandler(allHandlers);
deploymentManager = new DeploymentManager();
deploymentManager.setContextAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
deploymentManager.setContexts(contextHandlers);
server.addBean(deploymentManager);
}
/**
* Instantiates this object but does not perform any configuration. Used for unit testing.
*/
JettyServer(Server server, NiFiProperties properties) {
this.server = server;
this.props = properties;
}
private Handler loadInitialWars(final Set<Bundle> bundles) {
// load WARs
final Map<File, Bundle> warToBundleLookup = findWars(bundles);
// locate each war being deployed
File webUiWar = null;
File webApiWar = null;
File webErrorWar = null;
File webDocsWar = null;
File webContentViewerWar = null;
Map<File, Bundle> otherWars = new HashMap<>();
for (Map.Entry<File, Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
final File war = warBundleEntry.getKey();
final Bundle warBundle = warBundleEntry.getValue();
if (war.getName().toLowerCase().startsWith("nifi-web-api")) {
webApiWar = war;
} else if (war.getName().toLowerCase().startsWith("nifi-web-error")) {
webErrorWar = war;
} else if (war.getName().toLowerCase().startsWith("nifi-web-docs")) {
webDocsWar = war;
} else if (war.getName().toLowerCase().startsWith("nifi-web-content-viewer")) {
webContentViewerWar = war;
} else if (war.getName().toLowerCase().startsWith("nifi-web")) {
webUiWar = war;
} else {
otherWars.put(war, warBundle);
}
}
// ensure the required wars were found
if (webUiWar == null) {
throw new RuntimeException("Unable to load nifi-web WAR");
} else if (webApiWar == null) {
throw new RuntimeException("Unable to load nifi-web-api WAR");
} else if (webDocsWar == null) {
throw new RuntimeException("Unable to load nifi-web-docs WAR");
} else if (webErrorWar == null) {
throw new RuntimeException("Unable to load nifi-web-error WAR");
} else if (webContentViewerWar == null) {
throw new RuntimeException("Unable to load nifi-web-content-viewer WAR");
}
// handlers for each war and init params for the web api
final ExtensionUiInfo extensionUiInfo = loadWars(otherWars);
componentUiExtensionWebContexts = new ArrayList<>(extensionUiInfo.getComponentUiExtensionWebContexts());
contentViewerWebContexts = new ArrayList<>(extensionUiInfo.getContentViewerWebContexts());
componentUiExtensions = new UiExtensionMapping(extensionUiInfo.getComponentUiExtensionsByType());
final HandlerCollection webAppContextHandlers = new HandlerCollection();
final Collection<WebAppContext> extensionUiContexts = extensionUiInfo.getWebAppContexts();
extensionUiContexts.forEach(webAppContextHandlers::addHandler);
final ClassLoader frameworkClassLoader = getClass().getClassLoader();
// load the web ui app
final WebAppContext webUiContext = loadWar(webUiWar, CONTEXT_PATH_NIFI, frameworkClassLoader);
webUiContext.getInitParams().put("oidc-supported", String.valueOf(props.isOidcEnabled()));
webUiContext.getInitParams().put("knox-supported", String.valueOf(props.isKnoxSsoEnabled()));
webUiContext.getInitParams().put("saml-supported", String.valueOf(props.isSamlEnabled()));
webUiContext.getInitParams().put("saml-single-logout-supported", String.valueOf(props.isSamlSingleLogoutEnabled()));
webUiContext.getInitParams().put("allowedContextPaths", props.getAllowedContextPaths());
webAppContextHandlers.addHandler(webUiContext);
// load the web api app
webApiContext = loadWar(webApiWar, CONTEXT_PATH_NIFI_API, frameworkClassLoader);
webAppContextHandlers.addHandler(webApiContext);
// load the content viewer app
webContentViewerContext = loadWar(webContentViewerWar, CONTEXT_PATH_NIFI_CONTENT_VIEWER, frameworkClassLoader);
webContentViewerContext.getInitParams().putAll(extensionUiInfo.getMimeMappings());
webAppContextHandlers.addHandler(webContentViewerContext);
// load the documentation war
webDocsContext = loadWar(webDocsWar, CONTEXT_PATH_NIFI_DOCS, frameworkClassLoader);
// add the servlets which serve the HTML documentation within the documentation web app
addDocsServlets(webDocsContext);
webAppContextHandlers.addHandler(webDocsContext);
// load the web error app
final WebAppContext webErrorContext = loadWar(webErrorWar, CONTEXT_PATH_ROOT, frameworkClassLoader);
webErrorContext.getInitParams().put("allowedContextPaths", props.getAllowedContextPaths());
webAppContextHandlers.addHandler(webErrorContext);
// deploy the web apps
return gzip(webAppContextHandlers);
}
@Override
public void loadExtensionUis(final Set<Bundle> bundles) {
// Find and load any WARs contained within the set of bundles...
final Map<File, Bundle> warToBundleLookup = findWars(bundles);
final ExtensionUiInfo extensionUiInfo = loadWars(warToBundleLookup);
final Collection<WebAppContext> webAppContexts = extensionUiInfo.getWebAppContexts();
if (CollectionUtils.isEmpty(webAppContexts)) {
logger.debug("No webapp contexts were loaded, returning...");
return;
}
// Deploy each WAR that was loaded...
for (final WebAppContext webAppContext : webAppContexts) {
final App extensionUiApp = new App(deploymentManager, null, "nifi-jetty-server", webAppContext);
deploymentManager.addApp(extensionUiApp);
}
final Collection<WebAppContext> componentUiExtensionWebContexts = extensionUiInfo.getComponentUiExtensionWebContexts();
final Collection<WebAppContext> contentViewerWebContexts = extensionUiInfo.getContentViewerWebContexts();
// Inject the configuration context and security filter into contexts that need it
final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
// Merge results of current loading into previously loaded results...
this.componentUiExtensionWebContexts.addAll(componentUiExtensionWebContexts);
this.contentViewerWebContexts.addAll(contentViewerWebContexts);
this.componentUiExtensions.addUiExtensions(extensionUiInfo.getComponentUiExtensionsByType());
for (final WebAppContext webAppContext : webAppContexts) {
final Throwable t = webAppContext.getUnavailableException();
if (t != null) {
logger.error("Unable to start context due to " + t.getMessage(), t);
}
}
}
private ExtensionUiInfo loadWars(final Map<File, Bundle> warToBundleLookup) {
// handlers for each war and init params for the web api
final List<WebAppContext> webAppContexts = new ArrayList<>();
final Map<String, String> mimeMappings = new HashMap<>();
final Collection<WebAppContext> componentUiExtensionWebContexts = new ArrayList<>();
final Collection<WebAppContext> contentViewerWebContexts = new ArrayList<>();
final Map<String, List<UiExtension>> componentUiExtensionsByType = new HashMap<>();
final ClassLoader frameworkClassLoader = getClass().getClassLoader();
final ClassLoader jettyClassLoader = frameworkClassLoader.getParent();
// deploy the other wars
if (!warToBundleLookup.isEmpty()) {
// ui extension organized by component type
for (Map.Entry<File, Bundle> warBundleEntry : warToBundleLookup.entrySet()) {
final File war = warBundleEntry.getKey();
final Bundle warBundle = warBundleEntry.getValue();
// identify all known extension types in the war
final Map<UiExtensionType, List<String>> uiExtensionInWar = new HashMap<>();
identifyUiExtensionsForComponents(uiExtensionInWar, war);
// only include wars that are for custom processor ui's
if (!uiExtensionInWar.isEmpty()) {
// get the context path
String warName = StringUtils.substringBeforeLast(war.getName(), ".");
String warContextPath = String.format("/%s", warName);
// get the classloader for this war
ClassLoader narClassLoaderForWar = warBundle.getClassLoader();
// this should never be null
if (narClassLoaderForWar == null) {
narClassLoaderForWar = jettyClassLoader;
}
// create the extension web app context
WebAppContext extensionUiContext = loadWar(war, warContextPath, narClassLoaderForWar);
// create the ui extensions
for (final Map.Entry<UiExtensionType, List<String>> entry : uiExtensionInWar.entrySet()) {
final UiExtensionType extensionType = entry.getKey();
final List<String> types = entry.getValue();
if (UiExtensionType.ContentViewer.equals(extensionType)) {
// consider each content type identified
for (final String contentType : types) {
// map the content type to the context path
mimeMappings.put(contentType, warContextPath);
}
// this ui extension provides a content viewer
contentViewerWebContexts.add(extensionUiContext);
} else {
// consider each component type identified
for (final String componentTypeCoordinates : types) {
logger.info(String.format("Loading UI extension [%s, %s] for %s", extensionType, warContextPath, componentTypeCoordinates));
// record the extension definition
final UiExtension uiExtension = new UiExtension(extensionType, warContextPath);
// create if this is the first extension for this component type
List<UiExtension> componentUiExtensionsForType = componentUiExtensionsByType.get(componentTypeCoordinates);
if (componentUiExtensionsForType == null) {
componentUiExtensionsForType = new ArrayList<>();
componentUiExtensionsByType.put(componentTypeCoordinates, componentUiExtensionsForType);
}
// see if there is already a ui extension of this same time
if (containsUiExtensionType(componentUiExtensionsForType, extensionType)) {
throw new IllegalStateException(String.format("Encountered duplicate UI for %s", componentTypeCoordinates));
}
// record this extension
componentUiExtensionsForType.add(uiExtension);
}
// this ui extension provides a component custom ui
componentUiExtensionWebContexts.add(extensionUiContext);
}
}
// include custom ui web context in the handlers
webAppContexts.add(extensionUiContext);
}
}
}
return new ExtensionUiInfo(webAppContexts, mimeMappings, componentUiExtensionWebContexts, contentViewerWebContexts, componentUiExtensionsByType);
}
/**
* Returns whether or not the specified ui extensions already contains an extension of the specified type.
*
* @param componentUiExtensionsForType ui extensions for the type
* @param extensionType type of ui extension
* @return whether or not the specified ui extensions already contains an extension of the specified type
*/
private boolean containsUiExtensionType(final List<UiExtension> componentUiExtensionsForType, final UiExtensionType extensionType) {
for (final UiExtension uiExtension : componentUiExtensionsForType) {
if (extensionType.equals(uiExtension.getExtensionType())) {
return true;
}
}
return false;
}
/**
* Enables compression for the specified handler.
*
* @param handler handler to enable compression for
* @return compression enabled handler
*/
private Handler gzip(final Handler handler) {
final GzipHandler gzip = new GzipHandler();
gzip.setIncludedMethods("GET", "POST", "PUT", "DELETE");
gzip.setHandler(handler);
return gzip;
}
private Map<File, Bundle> findWars(final Set<Bundle> bundles) {
final Map<File, Bundle> wars = new HashMap<>();
// consider each nar working directory
bundles.forEach(bundle -> {
final BundleDetails details = bundle.getBundleDetails();
final File narDependencies = new File(details.getWorkingDirectory(), "NAR-INF/bundled-dependencies");
logger.debug("Attempting to load bundle {} from {}", details, narDependencies.getAbsolutePath());
if (narDependencies.isDirectory()) {
// list the wars from this nar
final File[] narDependencyDirs = narDependencies.listFiles(WAR_FILTER);
if (narDependencyDirs == null) {
throw new IllegalStateException(String.format("Unable to access working directory for NAR dependencies in: %s", narDependencies.getAbsolutePath()));
}
if (logger.isDebugEnabled()) {
logger.debug("Found {} available WARs in {}", narDependencyDirs.length, narDependencies.getAbsolutePath());
for (File f : narDependencyDirs) {
logger.debug("\t" + f.getAbsolutePath());
}
}
// add each war
for (final File war : narDependencyDirs) {
wars.put(war, bundle);
}
}
});
return wars;
}
private void readUiExtensions(final Map<UiExtensionType, List<String>> uiExtensions, final UiExtensionType uiExtensionType, final JarFile jarFile, final JarEntry jarEntry) throws IOException {
if (jarEntry == null) {
return;
}
// get an input stream for the nifi-processor configuration file
try (BufferedReader in = new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry)))) {
// read in each configured type
String rawComponentType;
while ((rawComponentType = in.readLine()) != null) {
// extract the component type
final String componentType = extractComponentType(rawComponentType);
if (componentType != null) {
List<String> extensions = uiExtensions.get(uiExtensionType);
// if there are currently no extensions for this type create it
if (extensions == null) {
extensions = new ArrayList<>();
uiExtensions.put(uiExtensionType, extensions);
}
// add the specified type
extensions.add(componentType);
}
}
}
}
/**
* Identifies all known UI extensions and stores them in the specified map.
*
* @param uiExtensions extensions
* @param warFile war
*/
private void identifyUiExtensionsForComponents(final Map<UiExtensionType, List<String>> uiExtensions, final File warFile) {
try (final JarFile jarFile = new JarFile(warFile)) {
// locate the ui extensions
readUiExtensions(uiExtensions, UiExtensionType.ContentViewer, jarFile, jarFile.getJarEntry("META-INF/nifi-content-viewer"));
readUiExtensions(uiExtensions, UiExtensionType.ProcessorConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-processor-configuration"));
readUiExtensions(uiExtensions, UiExtensionType.ControllerServiceConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-controller-service-configuration"));
readUiExtensions(uiExtensions, UiExtensionType.ReportingTaskConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-reporting-task-configuration"));
} catch (IOException ioe) {
logger.warn(String.format("Unable to inspect %s for a UI extensions.", warFile));
}
}
/**
* Extracts the component type. Trims the line and considers comments.
* Returns null if no type was found.
*
* @param line line
* @return type
*/
private String extractComponentType(final String line) {
final String trimmedLine = line.trim();
if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) {
final int indexOfPound = trimmedLine.indexOf("#");
return (indexOfPound > 0) ? trimmedLine.substring(0, indexOfPound) : trimmedLine;
}
return null;
}
private WebAppContext loadWar(final File warFile, final String contextPath, final ClassLoader parentClassLoader) {
final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath);
webappContext.setContextPath(contextPath);
webappContext.setDisplayName(contextPath);
// instruction jetty to examine these jars for tlds, web-fragments, etc
webappContext.setAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE);
// remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib
List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses()));
serverClasses.remove("org.slf4j.");
webappContext.setServerClasses(serverClasses.toArray(new String[0]));
webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML);
webappContext.getMimeTypes().addMimeMapping("ttf", "font/ttf");
// get the temp directory for this webapp
File tempDir = new File(props.getWebWorkingDirectory(), warFile.getName());
if (tempDir.exists() && !tempDir.isDirectory()) {
throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory");
} else if (!tempDir.exists()) {
final boolean made = tempDir.mkdirs();
if (!made) {
throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created");
}
}
if (!(tempDir.canRead() && tempDir.canWrite())) {
throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege");
}
// configure the temp dir
webappContext.setTempDirectory(tempDir);
// configure the max form size (3x the default)
webappContext.setMaxFormContentSize(600000);
// add HTTP security headers to all responses
// TODO: Allow more granular path configuration (e.g. /nifi-api/site-to-site/ vs. /nifi-api/process-groups)
ArrayList<Class<? extends Filter>> filters =
new ArrayList<>(Arrays.asList(
XFrameOptionsFilter.class,
ContentSecurityPolicyFilter.class,
XSSProtectionFilter.class,
XContentTypeOptionsFilter.class));
if(props.isHTTPSConfigured()) {
filters.add(StrictTransportSecurityFilter.class);
}
filters.forEach((filter) -> addFilters(filter, webappContext));
addDenialOfServiceFilters(webappContext, props);
if (CONTEXT_PATH_NIFI_API.equals(contextPath)) {
addAccessTokenRequestFilter(webappContext, props);
}
try {
// configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ...
webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext));
} catch (final IOException ioe) {
startUpFailure(ioe);
}
logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
return webappContext;
}
private void addFilters(Class<? extends Filter> clazz, WebAppContext webappContext) {
FilterHolder holder = new FilterHolder(clazz);
holder.setName(clazz.getSimpleName());
webappContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
}
private void addDocsServlets(WebAppContext docsContext) {
try {
// Load the nifi/docs directory
final File docsDir = getDocsDir("docs");
// load the component documentation working directory
final File componentDocsDirPath = props.getComponentDocumentationWorkingDirectory();
final File workingDocsDirectory = getWorkingDocsDirectory(componentDocsDirPath);
// Load the API docs
final File webApiDocsDir = getWebApiDocsDir();
// Create the servlet which will serve the static resources
ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class);
defaultHolder.setInitParameter("dirAllowed", "false");
ServletHolder docs = new ServletHolder("docs", DefaultServlet.class);
docs.setInitParameter("resourceBase", docsDir.getPath());
docs.setInitParameter("dirAllowed", "false");
ServletHolder components = new ServletHolder("components", DefaultServlet.class);
components.setInitParameter("resourceBase", workingDocsDirectory.getPath());
components.setInitParameter("dirAllowed", "false");
ServletHolder restApi = new ServletHolder("rest-api", DefaultServlet.class);
restApi.setInitParameter("resourceBase", webApiDocsDir.getPath());
restApi.setInitParameter("dirAllowed", "false");
docsContext.addServlet(docs, "/html/*");
docsContext.addServlet(components, "/components/*");
docsContext.addServlet(restApi, "/rest-api/*");
docsContext.addServlet(defaultHolder, "/");
logger.info("Loading documents web app with context path set to " + docsContext.getContextPath());
} catch (Exception ex) {
logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage());
startUpFailure(ex);
}
}
/**
* Adds configurable filters relating to preventing denial of service attacks to the given context.
* Currently, this implementation adds
* {@link org.eclipse.jetty.servlets.DoSFilter} and {@link ContentLengthFilter} filters.
*
* @param webAppContext context to which filters will be added
* @param props the {@link NiFiProperties}
*/
private static void addDenialOfServiceFilters(final WebAppContext webAppContext, final NiFiProperties props) {
addWebRequestLimitingFilter(webAppContext, props.getMaxWebRequestsPerSecond(), getWebRequestTimeoutMs(props), props.getWebRequestIpWhitelist());
// Only add the ContentLengthFilter if the property is explicitly set (empty by default)
final int maxRequestSize = determineMaxRequestSize(props);
if (maxRequestSize > 0) {
addContentLengthFilter(webAppContext, maxRequestSize);
} else {
logger.debug("Not adding content-length filter because {} is not set in nifi.properties", NiFiProperties.WEB_MAX_CONTENT_SIZE);
}
}
private static long getWebRequestTimeoutMs(final NiFiProperties props) {
final long defaultRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_WEB_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS));
long configuredRequestTimeout = 0L;
try {
configuredRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(props.getWebRequestTimeout(), TimeUnit.MILLISECONDS));
} catch (final NumberFormatException e) {
logger.warn("Exception parsing property [{}]; using default value: [{}]", NiFiProperties.WEB_REQUEST_TIMEOUT, defaultRequestTimeout);
}
return configuredRequestTimeout > 0 ? configuredRequestTimeout : defaultRequestTimeout;
}
/**
* Adds the {@link org.eclipse.jetty.servlets.DoSFilter} to the specified context and path. Limits incoming web requests to {@code maxWebRequestsPerSecond} per second.
* In order to allow clients to make more requests than the maximum rate, clients can be added to the {@code ipWhitelist}.
* The {@code requestTimeoutInMilliseconds} value limits requests to the given request timeout amount, and will close connections that run longer than this time.
*
* @param webAppContext Web Application Context where Filter will be added
* @param maxRequestsPerSec Maximum number of allowed requests per second
* @param maxRequestMs Maximum amount of time in milliseconds before a connection will be automatically closed
* @param allowed Comma-separated string of IP addresses that should not be rate limited. Does not apply to request timeout
*/
private static void addWebRequestLimitingFilter(final WebAppContext webAppContext, final int maxRequestsPerSec, final long maxRequestMs, final String allowed) {
final FilterHolder holder = new FilterHolder(DoSFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
put("maxRequestMs", Long.toString(maxRequestMs));
put("ipWhitelist", allowed);
}});
holder.setName(DoSFilter.class.getSimpleName());
webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", CONTEXT_PATH_ALL, maxRequestsPerSec, maxRequestMs, allowed);
}
private static void addAccessTokenRequestFilter(final WebAppContext webAppContext, final NiFiProperties properties) {
final int maxRequestsPerSec = properties.getMaxWebAccessTokenRequestsPerSecond();
final long maxRequestMs = getWebRequestTimeoutMs(properties);
final String webRequestAllowed = properties.getWebRequestIpWhitelist();
final FilterHolder holder = new FilterHolder(DoSFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
put("maxRequestMs", Long.toString(maxRequestMs));
put("ipWhitelist", webRequestAllowed);
put("maxWaitMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
put("delayMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
}});
holder.setName("AccessTokenRequest-DoSFilter");
webAppContext.addFilter(holder, RELATIVE_PATH_ACCESS_TOKEN, EnumSet.allOf(DispatcherType.class));
logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", RELATIVE_PATH_ACCESS_TOKEN, maxRequestsPerSec, maxRequestMs, webRequestAllowed);
}
private static int determineMaxRequestSize(NiFiProperties props) {
try {
final String webMaxContentSize = props.getWebMaxContentSize();
logger.debug("Read {} as {}", NiFiProperties.WEB_MAX_CONTENT_SIZE, webMaxContentSize);
if (StringUtils.isNotBlank(webMaxContentSize)) {
int configuredMaxRequestSize = DataUnit.parseDataSize(webMaxContentSize, DataUnit.B).intValue();
logger.debug("Parsed max content length as {} bytes", configuredMaxRequestSize);
return configuredMaxRequestSize;
} else {
logger.debug("{} read from nifi.properties is empty", NiFiProperties.WEB_MAX_CONTENT_SIZE);
}
} catch (final IllegalArgumentException e) {
logger.warn("Exception parsing property {}; disabling content length filter", NiFiProperties.WEB_MAX_CONTENT_SIZE);
logger.debug("Error during parsing: ", e);
}
return -1;
}
private static void addContentLengthFilter(final WebAppContext webAppContext, int maxContentLength) {
final FilterHolder holder = new FilterHolder(ContentLengthFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
put("maxContentLength", String.valueOf(maxContentLength));
}});
holder.setName(ContentLengthFilter.class.getSimpleName());
logger.debug("Adding ContentLengthFilter to Path [{}] with Maximum Content Length [{}B]", CONTEXT_PATH_ALL, maxContentLength);
webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
}
/**
* Returns a File object for the directory containing NIFI documentation.
* <p>
* Formerly, if the docsDirectory did not exist NIFI would fail to start
* with an IllegalStateException and a rather unhelpful log message.
* NIFI-2184 updates the process such that if the docsDirectory does not
* exist an attempt will be made to create the directory. If that is
* successful NIFI will no longer fail and will start successfully barring
* any other errors. The side effect of the docsDirectory not being present
* is that the documentation links under the 'General' portion of the help
* page will not be accessible, but at least the process will be running.
*
* @param docsDirectory Name of documentation directory in installation directory.
* @return A File object to the documentation directory; else startUpFailure called.
*/
private File getDocsDir(final String docsDirectory) {
File docsDir;
try {
docsDir = Paths.get(docsDirectory).toRealPath().toFile();
} catch (IOException ex) {
logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable.");
docsDir = new File(docsDirectory).getAbsoluteFile();
final boolean made = docsDir.mkdirs();
if (!made) {
logger.error("Failed to create 'docs' directory!");
startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created"));
}
}
return docsDir;
}
private File getWorkingDocsDirectory(final File componentDocsDirPath) {
File workingDocsDirectory = null;
try {
workingDocsDirectory = componentDocsDirPath.toPath().toRealPath().getParent().toFile();
} catch (IOException ex) {
logger.error("Failed to load :" + componentDocsDirPath.getAbsolutePath());
startUpFailure(ex);
}
return workingDocsDirectory;
}
private File getWebApiDocsDir() {
// load the rest documentation
final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
if (!webApiDocsDir.exists()) {
final boolean made = webApiDocsDir.mkdirs();
if (!made) {
logger.error("Failed to create " + webApiDocsDir.getAbsolutePath());
startUpFailure(new IOException(webApiDocsDir.getAbsolutePath() + " could not be created"));
}
}
return webApiDocsDir;
}
private void configureConnectors(final Server server) throws ServerConfigurationException {
// create the http configuration
final HttpConfiguration httpConfiguration = new HttpConfiguration();
final int headerSize = DataUnit.parseDataSize(props.getWebMaxHeaderSize(), DataUnit.B).intValue();
httpConfiguration.setRequestHeaderSize(headerSize);
httpConfiguration.setResponseHeaderSize(headerSize);
httpConfiguration.setSendServerVersion(props.shouldSendServerVersion());
// Check if both HTTP and HTTPS connectors are configured and fail if both are configured
if (bothHttpAndHttpsConnectorsConfigured(props)) {
logger.error("NiFi only supports one mode of HTTP or HTTPS operation, not both simultaneously. " +
"Check the nifi.properties file and ensure that either the HTTP hostname and port or the HTTPS hostname and port are empty");
startUpFailure(new IllegalStateException("Only one of the HTTP and HTTPS connectors can be configured at one time"));
}
if (props.getSslPort() != null) {
configureHttpsConnector(server, httpConfiguration);
} else if (props.getPort() != null) {
configureHttpConnector(server, httpConfiguration);
} else {
logger.error("Neither the HTTP nor HTTPS connector was configured in nifi.properties");
startUpFailure(new IllegalStateException("Must configure HTTP or HTTPS connector"));
}
}
/**
* Configures an HTTPS connector and adds it to the server.
*
* @param server the Jetty server instance
* @param httpConfiguration the configuration object for the HTTPS protocol settings
*/
private void configureHttpsConnector(Server server, HttpConfiguration httpConfiguration) {
String hostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
final Integer port = props.getSslPort();
String connectorLabel = "HTTPS";
final Map<String, String> httpsNetworkInterfaces = props.getHttpsNetworkInterfaces();
ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port);
configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc);
if (props.isSecurityAutoReloadEnabled()) {
configureSslContextFactoryReloading(server);
}
}
/**
* Configures a KeyStoreScanner and TrustStoreScanner at the configured reload intervals. This will
* reload the SSLContextFactory if any changes are detected to the keystore or truststore.
* @param server The Jetty server
*/
private void configureSslContextFactoryReloading(Server server) {
final int scanIntervalSeconds = Double.valueOf(FormatUtils.getPreciseTimeDuration(
props.getSecurityAutoReloadInterval(), TimeUnit.SECONDS))
.intValue();
final KeyStoreScanner keyStoreScanner = new KeyStoreScanner(sslContextFactory);
keyStoreScanner.setScanInterval(scanIntervalSeconds);
server.addBean(keyStoreScanner);
final TrustStoreScanner trustStoreScanner = new TrustStoreScanner(sslContextFactory);
trustStoreScanner.setScanInterval(scanIntervalSeconds);
server.addBean(trustStoreScanner);
}
/**
* Configures an HTTP connector and adds it to the server.
*
* @param server the Jetty server instance
* @param httpConfiguration the configuration object for the HTTP protocol settings
*/
private void configureHttpConnector(Server server, HttpConfiguration httpConfiguration) {
String hostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
final Integer port = props.getPort();
String connectorLabel = "HTTP";
final Map<String, String> httpNetworkInterfaces = props.getHttpNetworkInterfaces();
ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> new ServerConnector(s, new HttpConnectionFactory(c));
configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpNetworkInterfaces, scc);
}
/**
* Configures an HTTP(S) connector for the server given the provided parameters. The functionality between HTTP and HTTPS connectors is largely similar.
* 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.
*
* @param server the Jetty server instance
* @param configuration the HTTP/HTTPS configuration instance
* @param hostname the hostname from the nifi.properties file
* @param port the port to expose
* @param connectorLabel used for log output (e.g. "HTTP" or "HTTPS")
* @param networkInterfaces the map of network interfaces from nifi.properties
* @param serverConnectorCreator a function which accepts a {@code Server} and {@code HttpConnection} instance and returns a {@code ServerConnector}
*/
private void configureGenericConnector(Server server, HttpConfiguration configuration, String hostname, Integer port, String connectorLabel, Map<String, String> networkInterfaces,
ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> serverConnectorCreator) {
if (port < 0 || (int) Math.pow(2, 16) <= port) {
throw new ServerConfigurationException("Invalid " + connectorLabel + " port: " + port);
}
logger.info("Configuring Jetty for " + connectorLabel + " on port: " + port);
final List<Connector> serverConnectors = Lists.newArrayList();
// Calculate Idle Timeout as twice the auto-refresh interval. This ensures that even with some variance in timing,
// we are able to avoid closing connections from users' browsers most of the time. This can make a significant difference
// in HTTPS connections, as each HTTPS connection that is established must perform the SSL handshake.
final String autoRefreshInterval = props.getAutoRefreshInterval();
final long autoRefreshMillis = autoRefreshInterval == null ? 30000L : FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS);
final long idleTimeout = autoRefreshMillis * 2;
// If the interfaces collection is empty or each element is empty
if (networkInterfaces.isEmpty() || networkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) {
final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
// Set host and port
if (StringUtils.isNotBlank(hostname)) {
serverConnector.setHost(hostname);
}
serverConnector.setPort(port);
serverConnector.setIdleTimeout(idleTimeout);
serverConnectors.add(serverConnector);
} else {
// Add connectors for all IPs from network interfaces
serverConnectors.addAll(Lists.newArrayList(networkInterfaces.values().stream().map(ifaceName -> {
NetworkInterface iface = null;
try {
iface = NetworkInterface.getByName(ifaceName);
} catch (SocketException e) {
logger.error("Unable to get network interface by name {}", ifaceName, e);
}
if (iface == null) {
logger.warn("Unable to find network interface named {}", ifaceName);
}
return iface;
}).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream())
.map(inetAddress -> {
final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
// Set host and port
serverConnector.setHost(inetAddress.getHostAddress());
serverConnector.setPort(port);
serverConnector.setIdleTimeout(idleTimeout);
return serverConnector;
}).collect(Collectors.toList())));
}
// Add all connectors
serverConnectors.forEach(server::addConnector);
}
/**
* 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).
* Prints a warning log message with the relevant properties.
*
* @param props the NiFiProperties
* @return true if both ports are present
*/
static boolean bothHttpAndHttpsConnectorsConfigured(NiFiProperties props) {
Integer httpPort = props.getPort();
String httpHostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
Integer httpsPort = props.getSslPort();
String httpsHostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
if (httpPort != null && httpsPort != null) {
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");
logger.warn("HTTP connector: http://" + httpHostname + ":" + httpPort);
logger.warn("HTTPS connector: https://" + httpsHostname + ":" + httpsPort);
return true;
}
return false;
}
private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration, int port) {
// add some secure config
final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
httpsConfiguration.setSecureScheme("https");
httpsConfiguration.setSecurePort(port);
httpsConfiguration.setSendServerVersion(props.shouldSendServerVersion());
httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
// build the connector
return new ServerConnector(server,
new SslConnectionFactory(createSslContextFactory(), "http/1.1"),
new HttpConnectionFactory(httpsConfiguration));
}
private SslContextFactory createSslContextFactory() {
final SslContextFactory.Server serverContextFactory = new SslContextFactory.Server();
configureSslContextFactory(serverContextFactory, props);
this.sslContextFactory = serverContextFactory;
return serverContextFactory;
}
protected static void configureSslContextFactory(SslContextFactory.Server contextFactory, NiFiProperties props) {
// Explicitly exclude legacy TLS protocol versions
contextFactory.setIncludeProtocols(TlsConfiguration.getCurrentSupportedTlsProtocolVersions());
contextFactory.setExcludeProtocols("TLS", "TLSv1", "TLSv1.1", "SSL", "SSLv2", "SSLv2Hello", "SSLv3");
// on configuration, replace default application cipher suites with those configured
final String includeCipherSuitesProps = props.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE);
if (StringUtils.isNotEmpty(includeCipherSuitesProps)) {
final String[] includeCipherSuites = includeCipherSuitesProps.split(REGEX_SPLIT_PROPERTY);
logger.info("Setting include cipher suites from configuration; parsed property = [{}].",
StringUtils.join(includeCipherSuites, JOIN_ARRAY));
contextFactory.setIncludeCipherSuites(includeCipherSuites);
}
final String excludeCipherSuitesProps = props.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE);
if (StringUtils.isNotEmpty(excludeCipherSuitesProps)) {
final String[] excludeCipherSuites = excludeCipherSuitesProps.split(REGEX_SPLIT_PROPERTY);
logger.info("Setting exclude cipher suites from configuration; parsed property = [{}].",
StringUtils.join(excludeCipherSuites, JOIN_ARRAY));
contextFactory.setExcludeCipherSuites(excludeCipherSuites);
}
// require client auth when not supporting login, Kerberos service, or anonymous access
if (props.isClientAuthRequiredForRestApi()) {
contextFactory.setNeedClientAuth(true);
} else {
contextFactory.setWantClientAuth(true);
}
/* below code sets JSSE system properties when values are provided */
// keystore properties
if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_KEYSTORE))) {
contextFactory.setKeyStorePath(props.getProperty(NiFiProperties.SECURITY_KEYSTORE));
}
String keyStoreType = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE);
if (StringUtils.isNotBlank(keyStoreType)) {
contextFactory.setKeyStoreType(keyStoreType);
String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(keyStoreType);
if (StringUtils.isNoneEmpty(keyStoreProvider)) {
contextFactory.setKeyStoreProvider(keyStoreProvider);
}
}
final String keystorePassword = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD);
final String keyPassword = props.getProperty(NiFiProperties.SECURITY_KEY_PASSWD);
if (StringUtils.isNotBlank(keystorePassword)) {
// if no key password was provided, then assume the keystore password is the same as the key password.
final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword;
contextFactory.setKeyStorePassword(keystorePassword);
contextFactory.setKeyManagerPassword(defaultKeyPassword);
} else if (StringUtils.isNotBlank(keyPassword)) {
// since no keystore password was provided, there will be no keystore integrity check
contextFactory.setKeyManagerPassword(keyPassword);
}
// truststore properties
if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE))) {
contextFactory.setTrustStorePath(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE));
}
String trustStoreType = props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE);
if (StringUtils.isNotBlank(trustStoreType)) {
contextFactory.setTrustStoreType(trustStoreType);
String trustStoreProvider = KeyStoreUtils.getKeyStoreProvider(trustStoreType);
if (StringUtils.isNoneEmpty(trustStoreProvider)) {
contextFactory.setTrustStoreProvider(trustStoreProvider);
}
}
if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD))) {
contextFactory.setTrustStorePassword(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD));
}
}
@Override
public void start() {
try {
// Create a standard extension manager and discover extensions
final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager();
extensionManager.discoverExtensions(systemBundle, bundles);
extensionManager.logClassLoaderMapping();
// Set the extension manager into the holder which makes it available to the Spring context via a factory bean
ExtensionManagerHolder.init(extensionManager);
// Generate docs for extensions
DocGenerator.generate(props, extensionManager, extensionMapping);
// start the server
server.start();
// ensure everything started successfully
for (Handler handler : server.getChildHandlers()) {
// see if the handler is a web app
if (handler instanceof WebAppContext) {
WebAppContext context = (WebAppContext) handler;
// see if this webapp had any exceptions that would
// cause it to be unavailable
if (context.getUnavailableException() != null) {
startUpFailure(context.getUnavailableException());
}
}
}
// ensure the appropriate wars deployed successfully before injecting the NiFi context and security filters
// this must be done after starting the server (and ensuring there were no start up failures)
if (webApiContext != null) {
// give the web api the component ui extensions
final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext();
webApiServletContext.setAttribute("nifi-ui-extensions", componentUiExtensions);
// get the application context
final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext);
final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class);
final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain");
// component ui extensions
performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter);
// content viewer extensions
performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter);
// content viewer controller
if (webContentViewerContext != null) {
final ContentAccess contentAccess = webApplicationContext.getBean("contentAccess", ContentAccess.class);
// add the content access
final ServletContext webContentViewerServletContext = webContentViewerContext.getServletHandler().getServletContext();
webContentViewerServletContext.setAttribute("nifi-content-access", contentAccess);
if (securityFilter != null) {
webContentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
}
}
diagnosticsFactory = webApplicationContext.getBean("diagnosticsFactory", DiagnosticsFactory.class);
decommissionTask = webApplicationContext.getBean("decommissionTask", DecommissionTask.class);
}
// ensure the web document war was loaded and provide the extension mapping
if (webDocsContext != null) {
final ServletContext webDocsServletContext = webDocsContext.getServletHandler().getServletContext();
webDocsServletContext.setAttribute("nifi-extension-mapping", extensionMapping);
}
// if this nifi is a node in a cluster, start the flow service and load the flow - the
// flow service is loaded here for clustered nodes because the loading of the flow will
// initialize the connection between the node and the NCM. if the node connects (starts
// heartbeating, etc), the NCM may issue web requests before the application (wars) have
// finished loading. this results in the node being disconnected since its unable to
// successfully respond to the requests. to resolve this, flow loading was moved to here
// (after the wars have been successfully deployed) when this nifi instance is a node
// in a cluster
if (props.isNode()) {
FlowService flowService = null;
try {
logger.info("Loading Flow...");
ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(webApiContext.getServletContext());
flowService = ctx.getBean("flowService", FlowService.class);
// start and load the flow
flowService.start();
flowService.load(null);
logger.info("Flow loaded successfully.");
} catch (BeansException | LifeCycleStartException | IOException | FlowSerializationException | FlowSynchronizationException | UninheritableFlowException e) {
// ensure the flow service is terminated
if (flowService != null && flowService.isRunning()) {
flowService.stop(false);
}
logger.error("Unable to load flow due to: " + e, e);
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
}
}
final NarLoader narLoader = new StandardNarLoader(
props.getExtensionsWorkingDirectory(),
props.getComponentDocumentationWorkingDirectory(),
NarClassLoadersHolder.getInstance(),
extensionManager,
extensionMapping,
this);
narAutoLoader = new NarAutoLoader(props, narLoader, extensionManager);
narAutoLoader.start();
// dump the application url after confirming everything started successfully
dumpUrls();
} catch (Exception ex) {
startUpFailure(ex);
}
}
@Override
public DiagnosticsFactory getDiagnosticsFactory() {
// The diagnosticsFactory is initialized during server startup. If the diagnostics factory happens to be
// requested before the Server starts, or after the server fails to start, we cannot provide the fully initialized
// diagnostics factory. But it is still helpful to provide what we can, so we will provide the Thread Dump Factory.
return diagnosticsFactory == null ? getThreadDumpFactory() : diagnosticsFactory;
}
@Override
public DiagnosticsFactory getThreadDumpFactory() {
return new ThreadDumpDiagnosticsFactory();
}
@Override
public DecommissionTask getDecommissionTask() {
return decommissionTask;
}
private void performInjectionForComponentUis(final Collection<WebAppContext> componentUiExtensionWebContexts,
final NiFiWebConfigurationContext configurationContext, final FilterHolder securityFilter) {
if (CollectionUtils.isNotEmpty(componentUiExtensionWebContexts)) {
for (final WebAppContext customUiContext : componentUiExtensionWebContexts) {
// set the NiFi context in each custom ui servlet context
final ServletContext customUiServletContext = customUiContext.getServletHandler().getServletContext();
customUiServletContext.setAttribute("nifi-web-configuration-context", configurationContext);
// add the security filter to any ui extensions wars
if (securityFilter != null) {
customUiContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
}
}
}
}
private void performInjectionForContentViewerUis(final Collection<WebAppContext> contentViewerWebContexts,
final FilterHolder securityFilter) {
if (CollectionUtils.isNotEmpty(contentViewerWebContexts)) {
for (final WebAppContext contentViewerContext : contentViewerWebContexts) {
// add the security filter to any content viewer wars
if (securityFilter != null) {
contentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class));
}
}
}
}
private void dumpUrls() throws SocketException {
final List<String> urls = new ArrayList<>();
for (Connector connector : server.getConnectors()) {
if (connector instanceof ServerConnector) {
final ServerConnector serverConnector = (ServerConnector) connector;
Set<String> hosts = new HashSet<>();
// determine the hosts
if (StringUtils.isNotBlank(serverConnector.getHost())) {
hosts.add(serverConnector.getHost());
} else {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
if (networkInterfaces != null) {
for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
hosts.add(inetAddress.getHostAddress());
}
}
}
}
// ensure some hosts were found
if (!hosts.isEmpty()) {
String scheme = "http";
if (props.getSslPort() != null && serverConnector.getPort() == props.getSslPort()) {
scheme = "https";
}
// dump each url
for (String host : hosts) {
urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort()));
}
}
}
}
if (urls.isEmpty()) {
logger.warn("NiFi has started, but the UI is not available on any hosts. Please verify the host properties.");
} else {
// log the ui location
logger.info("NiFi has started. The UI is available at the following URLs:");
for (final String url : urls) {
logger.info(String.format("%s/nifi", url));
}
}
}
private void startUpFailure(Throwable t) {
System.err.println("Failed to start web server: " + t.getMessage());
System.err.println("Shutting down...");
logger.warn("Failed to start web server... shutting down.", t);
System.exit(1);
}
@Override
public void initialize(NiFiProperties properties, Bundle systemBundle, Set<Bundle> bundles, ExtensionMapping extensionMapping) {
this.props = properties;
this.systemBundle = systemBundle;
this.bundles = bundles;
this.extensionMapping = extensionMapping;
init();
}
@Override
public void stop() {
try {
server.stop();
} catch (Exception ex) {
logger.warn("Failed to stop web server", ex);
}
try {
if (narAutoLoader != null) {
narAutoLoader.stop();
}
} catch (Exception e) {
logger.warn("Failed to stop NAR auto-loader", e);
}
}
/**
* Holds the result of loading WARs for custom UIs.
*/
private static class ExtensionUiInfo {
private final Collection<WebAppContext> webAppContexts;
private final Map<String, String> mimeMappings;
private final Collection<WebAppContext> componentUiExtensionWebContexts;
private final Collection<WebAppContext> contentViewerWebContexts;
private final Map<String, List<UiExtension>> componentUiExtensionsByType;
public ExtensionUiInfo(final Collection<WebAppContext> webAppContexts,
final Map<String, String> mimeMappings,
final Collection<WebAppContext> componentUiExtensionWebContexts,
final Collection<WebAppContext> contentViewerWebContexts,
final Map<String, List<UiExtension>> componentUiExtensionsByType) {
this.webAppContexts = webAppContexts;
this.mimeMappings = mimeMappings;
this.componentUiExtensionWebContexts = componentUiExtensionWebContexts;
this.contentViewerWebContexts = contentViewerWebContexts;
this.componentUiExtensionsByType = componentUiExtensionsByType;
}
public Collection<WebAppContext> getWebAppContexts() {
return webAppContexts;
}
public Map<String, String> getMimeMappings() {
return mimeMappings;
}
public Collection<WebAppContext> getComponentUiExtensionWebContexts() {
return componentUiExtensionWebContexts;
}
public Collection<WebAppContext> getContentViewerWebContexts() {
return contentViewerWebContexts;
}
public Map<String, List<UiExtension>> getComponentUiExtensionsByType() {
return componentUiExtensionsByType;
}
}
private static class ThreadDumpDiagnosticsFactory implements DiagnosticsFactory {
@Override
public DiagnosticsDump create(final boolean verbose) {
return new DiagnosticsDump() {
@Override
public void writeTo(final OutputStream out) throws IOException {
final DiagnosticsDumpElement threadDumpElement = new ThreadDumpTask().captureDump(verbose);
final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
for (final String detail : threadDumpElement.getDetails()) {
writer.write(detail);
writer.write("\n");
}
writer.flush();
}
};
}
}
}
@FunctionalInterface
interface ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> {
ServerConnector create(Server server, HttpConfiguration httpConfiguration);
}