http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml new file mode 100644 index 0000000..98ad3ce --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<!-- + ~ 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. + --> +<authorizers> + + <userGroupProvider> + <identifier>file-user-group-provider</identifier> + <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class> + <property name="Users File">./target/test-classes/security/users.xml</property> + </userGroupProvider> + + <accessPolicyProvider> + <identifier>file-access-policy-provider</identifier> + <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class> + <property name="User Group Provider">file-user-group-provider</property> + <property name="Authorizations File">./target/test-classes/security/authorizations.xml</property> + </accessPolicyProvider> + + <authorizer> + <identifier>managed-authorizer</identifier> + <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class> + <property name="Access Policy Provider">file-access-policy-provider</property> + </authorizer> + +</authorizers> \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot new file mode 100644 index 0000000..ce1901f --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot @@ -0,0 +1,5 @@ +{ + "header": { + }, + "content": {} +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot new file mode 100644 index 0000000..33d4da3 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot @@ -0,0 +1,6 @@ +{ + "header": { + "dataModelVersion": "One" + }, + "content": {} +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot new file mode 100644 index 0000000..7c1ab49 Binary files /dev/null and b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot differ http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot new file mode 100644 index 0000000..7f4dfc5 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot @@ -0,0 +1,97 @@ +{ + "header": { + "dataModelVersion": 2 + }, + "content": { + "identifier": "a2c80883-171c-316d-ba25-24df2c352693", + "name": "Flow1", + "comments": "", + "position": { + "x": 1549.249149182042, + "y": 764.2426186568309 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "92fe4513-21c0-34f6-a916-2874f46ae864", + "name": "GenerateFlowFile", + "comments": "", + "position": { + "x": 488.99999411591034, + "y": 114.00000359389122 + }, + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "1.6.0-SNAPSHOT" + }, + "style": {}, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": { + "character-set": { + "name": "character-set", + "displayName": "Character Set", + "identifiesControllerService": false, + "sensitive": false + }, + "File Size": { + "name": "File Size", + "displayName": "File Size", + "identifiesControllerService": false, + "sensitive": false + }, + "generate-ff-custom-text": { + "name": "generate-ff-custom-text", + "displayName": "Custom Text", + "identifiesControllerService": false, + "sensitive": false + }, + "Batch Size": { + "name": "Batch Size", + "displayName": "Batch Size", + "identifiesControllerService": false, + "sensitive": false + }, + "Unique FlowFiles": { + "name": "Unique FlowFiles", + "displayName": "Unique FlowFiles", + "identifiesControllerService": false, + "sensitive": false + }, + "Data Format": { + "name": "Data Format", + "displayName": "Data Format", + "identifiesControllerService": false, + "sensitive": false + } + }, + "schedulingPeriod": "0 sec", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "componentType": "PROCESSOR", + "groupIdentifier": "a2c80883-171c-316d-ba25-24df2c352693" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "variables": {}, + "componentType": "PROCESS_GROUP" + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot new file mode 100644 index 0000000..574fe56 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot @@ -0,0 +1,6 @@ +{ + "header": { + "dataModelVersion": 3 + }, + "content": {} +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/pom.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-jetty/pom.xml b/nifi-registry-core/nifi-registry-jetty/pom.xml new file mode 100644 index 0000000..9c17c11 --- /dev/null +++ b/nifi-registry-core/nifi-registry-jetty/pom.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.nifi.registry</groupId> + <artifactId>nifi-registry-core</artifactId> + <version>0.3.0-SNAPSHOT</version> + </parent> + <artifactId>nifi-registry-jetty</artifactId> + <packaging>jar</packaging> + <dependencies> + <dependency> + <groupId>org.apache.nifi.registry</groupId> + <artifactId>nifi-registry-properties</artifactId> + <version>0.3.0-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-webapp</artifactId> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlets</artifactId> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-annotations</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>apache-jsp</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>apache-jstl</artifactId> + <scope>compile</scope> + </dependency> + </dependencies> +</project> http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java new file mode 100644 index 0000000..c202a5b --- /dev/null +++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java @@ -0,0 +1,489 @@ +/* + * 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.registry.jetty; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.crypto.CryptoKeyProvider; +import org.eclipse.jetty.annotations.AnnotationConfiguration; +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.ContextHandler; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceCollection; +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 java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + + +public class JettyServer { + + private static final Logger logger = LoggerFactory.getLogger(JettyServer.class); + private static final String WEB_DEFAULTS_XML = "org/apache/nifi-registry/web/webdefault.xml"; + private static final int HEADER_BUFFER_SIZE = 16 * 1024; // 16kb + + private static final FileFilter WAR_FILTER = new FileFilter() { + @Override + public boolean accept(File pathname) { + final String nameToTest = pathname.getName().toLowerCase(); + return nameToTest.endsWith(".war") && pathname.isFile(); + } + }; + + private final NiFiRegistryProperties properties; + private final CryptoKeyProvider masterKeyProvider; + private final Server server; + + private WebAppContext webUiContext; + private WebAppContext webApiContext; + private WebAppContext webDocsContext; + + public JettyServer(final NiFiRegistryProperties properties, final CryptoKeyProvider cryptoKeyProvider) { + final QueuedThreadPool threadPool = new QueuedThreadPool(properties.getWebThreads()); + threadPool.setName("NiFi Registry Web Server"); + + this.properties = properties; + this.masterKeyProvider = cryptoKeyProvider; + 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()); + + try { + configureConnectors(); + loadWars(); + } catch (final Throwable t) { + startUpFailure(t); + } + } + + private void configureConnectors() { + // create the http configuration + final HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setRequestHeaderSize(HEADER_BUFFER_SIZE); + httpConfiguration.setResponseHeaderSize(HEADER_BUFFER_SIZE); + + if (properties.getPort() != null) { + final Integer port = properties.getPort(); + if (port < 0 || (int) Math.pow(2, 16) <= port) { + throw new IllegalStateException("Invalid HTTP port: " + port); + } + + logger.info("Configuring Jetty for HTTP on port: " + port); + + // create the connector + final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); + + // set host and port + if (StringUtils.isNotBlank(properties.getHttpHost())) { + http.setHost(properties.getHttpHost()); + } + http.setPort(port); + + // add this connector + server.addConnector(http); + } else if (properties.getSslPort() != null) { + final Integer port = properties.getSslPort(); + if (port < 0 || (int) Math.pow(2, 16) <= port) { + throw new IllegalStateException("Invalid HTTPs port: " + port); + } + + if (StringUtils.isBlank(properties.getKeyStorePath())) { + throw new IllegalStateException(NiFiRegistryProperties.SECURITY_KEYSTORE + + " must be provided to configure Jetty for HTTPs"); + } + + logger.info("Configuring Jetty for HTTPs on port: " + port); + + // add some secure config + final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); + httpsConfiguration.setSecureScheme("https"); + httpsConfiguration.setSecurePort(properties.getSslPort()); + httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); + + // build the connector + final ServerConnector https = new ServerConnector(server, + new SslConnectionFactory(createSslContextFactory(), "http/1.1"), + new HttpConnectionFactory(httpsConfiguration)); + + // set host and port + if (StringUtils.isNotBlank(properties.getHttpsHost())) { + https.setHost(properties.getHttpsHost()); + } + https.setPort(port); + + // add this connector + server.addConnector(https); + } + } + + private SslContextFactory createSslContextFactory() { + final SslContextFactory contextFactory = new SslContextFactory(); + + // if needClientAuth is false then set want to true so we can optionally use certs + if (properties.getNeedClientAuth()) { + logger.info("Setting Jetty's SSLContextFactory needClientAuth to true"); + contextFactory.setNeedClientAuth(true); + } else { + logger.info("Setting Jetty's SSLContextFactory wantClientAuth to true"); + contextFactory.setWantClientAuth(true); + } + + /* below code sets JSSE system properties when values are provided */ + // keystore properties + if (StringUtils.isNotBlank(properties.getKeyStorePath())) { + contextFactory.setKeyStorePath(properties.getKeyStorePath()); + } + if (StringUtils.isNotBlank(properties.getKeyStoreType())) { + contextFactory.setKeyStoreType(properties.getKeyStoreType()); + } + final String keystorePassword = properties.getKeyStorePassword(); + final String keyPassword = properties.getKeyPassword(); + 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.setKeyManagerPassword(keystorePassword); + contextFactory.setKeyStorePassword(defaultKeyPassword); + } else if (StringUtils.isNotBlank(keyPassword)) { + // since no keystore password was provided, there will be no keystore integrity check + contextFactory.setKeyStorePassword(keyPassword); + } + + // truststore properties + if (StringUtils.isNotBlank(properties.getTrustStorePath())) { + contextFactory.setTrustStorePath(properties.getTrustStorePath()); + } + if (StringUtils.isNotBlank(properties.getTrustStoreType())) { + contextFactory.setTrustStoreType(properties.getTrustStoreType()); + } + if (StringUtils.isNotBlank(properties.getTrustStorePassword())) { + contextFactory.setTrustStorePassword(properties.getTrustStorePassword()); + } + + return contextFactory; + } + + private void loadWars() throws IOException { + final File warDirectory = properties.getWarLibDirectory(); + final File[] wars = warDirectory.listFiles(WAR_FILTER); + + if (wars == null) { + throw new RuntimeException("Unable to access war lib directory: " + warDirectory); + } + + File webUiWar = null; + File webApiWar = null; + File webDocsWar = null; + for (final File war : wars) { + if (war.getName().startsWith("nifi-registry-web-ui")) { + webUiWar = war; + } else if (war.getName().startsWith("nifi-registry-web-api")) { + webApiWar = war; + } else if (war.getName().startsWith("nifi-registry-web-docs")) { + webDocsWar = war; + } + } + + if (webUiWar == null) { + throw new IllegalStateException("Unable to locate NiFi Registry Web UI"); + } else if (webApiWar == null) { + throw new IllegalStateException("Unable to locate NiFi Registry Web API"); + } else if (webDocsWar == null) { + throw new IllegalStateException("Unable to locate NiFi Registry Web Docs"); + } + + webUiContext = loadWar(webUiWar, "/nifi-registry"); + + webApiContext = loadWar(webApiWar, "/nifi-registry-api", getWebApiAdditionalClasspath()); + logger.info("Adding {} object to ServletContext with key 'nifi-registry.properties'", properties.getClass().getSimpleName()); + webApiContext.setAttribute("nifi-registry.properties", properties); + logger.info("Adding {} object to ServletContext with key 'nifi-registry.key'", masterKeyProvider.getClass().getSimpleName()); + webApiContext.setAttribute("nifi-registry.key", masterKeyProvider); + + // there is an issue scanning the asm repackaged jar so narrow down what we are scanning + webApiContext.setAttribute("org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern", ".*/spring-[^/]*\\.jar$"); + + final String docsContextPath = "/nifi-registry-docs"; + webDocsContext = loadWar(webDocsWar, docsContextPath); + + final HandlerCollection handlers = new HandlerCollection(); + handlers.addHandler(webUiContext); + handlers.addHandler(webApiContext); + handlers.addHandler(createDocsWebApp(docsContextPath)); + handlers.addHandler(webDocsContext); + server.setHandler(handlers); + } + + private WebAppContext loadWar(final File warFile, final String contextPath) + throws IOException { + return loadWar(warFile, contextPath, new URL[0]); + } + + private WebAppContext loadWar(final File warFile, final String contextPath, final URL[] additionalResources) + throws IOException { + final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath); + webappContext.setContextPath(contextPath); + webappContext.setDisplayName(contextPath); + + // 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); + + // get the temp directory for this webapp + final File webWorkingDirectory = properties.getWebWorkingDirectory(); + final File tempDir = new File(webWorkingDirectory, 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); + + // start out assuming the system ClassLoader will be the parent, but if additional resources were specified then + // inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources + ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader(); + if (additionalResources != null && additionalResources.length > 0) { + URLClassLoader additionalClassLoader = new URLClassLoader(additionalResources, ClassLoader.getSystemClassLoader()); + parentClassLoader = additionalClassLoader; + } + + webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext)); + + logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath); + return webappContext; + } + + private URL[] getWebApiAdditionalClasspath() { + final String dbDriverDir = properties.getDatabaseDriverDirectory(); + + if (StringUtils.isBlank(dbDriverDir)) { + logger.info("No database driver directory was specified"); + return new URL[0]; + } + + final File dirFile = new File(dbDriverDir); + + if (!dirFile.exists()) { + logger.warn("Skipping database driver directory that does not exist: " + dbDriverDir); + return new URL[0]; + } + + if (!dirFile.canRead()) { + logger.warn("Skipping database driver directory that can not be read: " + dbDriverDir); + return new URL[0]; + } + + final List<URL> resources = new LinkedList<>(); + try { + resources.add(dirFile.toURI().toURL()); + } catch (final MalformedURLException mfe) { + logger.warn("Unable to add {} to classpath due to {}", new Object[]{ dirFile.getAbsolutePath(), mfe.getMessage()}, mfe); + } + + if (dirFile.isDirectory()) { + final File[] files = dirFile.listFiles(); + if (files != null) { + for (final File resource : files) { + if (resource.isDirectory()) { + logger.warn("Recursive directories are not supported, skipping " + resource.getAbsolutePath()); + } else { + try { + resources.add(resource.toURI().toURL()); + } catch (final MalformedURLException mfe) { + logger.warn("Unable to add {} to classpath due to {}", new Object[]{ resource.getAbsolutePath(), mfe.getMessage()}, mfe); + } + } + } + } + } + + if (!resources.isEmpty()) { + logger.info("Added additional resources to nifi-registry-api classpath: ["); + for (URL resource : resources) { + logger.info(" " + resource.toString()); + } + logger.info("]"); + } + + return resources.toArray(new URL[resources.size()]); + } + + private ContextHandler createDocsWebApp(final String contextPath) throws IOException { + final ResourceHandler resourceHandler = new ResourceHandler(); + resourceHandler.setDirectoriesListed(false); + + // load the docs directory + final File docsDir = Paths.get("docs").toRealPath().toFile(); + final Resource docsResource = Resource.newResource(docsDir); + + // load the rest documentation + final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs"); + if (!webApiDocsDir.exists()) { + final boolean made = webApiDocsDir.mkdirs(); + if (!made) { + throw new RuntimeException(webApiDocsDir.getAbsolutePath() + " could not be created"); + } + } + final Resource webApiDocsResource = Resource.newResource(webApiDocsDir); + + // create resources for both docs locations + final ResourceCollection resources = new ResourceCollection(docsResource, webApiDocsResource); + resourceHandler.setBaseResource(resources); + + // create the context handler + final ContextHandler handler = new ContextHandler(contextPath); + handler.setHandler(resourceHandler); + + logger.info("Loading documents web app with context path set to " + contextPath); + return handler; + } + + public void start() { + try { + // 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()); + } + } + } + + dumpUrls(); + } catch (final Throwable t) { + startUpFailure(t); + } + } + + 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); + } + + 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 (properties.getSslPort() != null && serverConnector.getPort() == properties.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 Registry has started, but the UI is not available on any hosts. Please verify the host properties."); + } else { + // log the ui location + logger.info("NiFi Registry has started. The UI is available at the following URLs:"); + for (final String url : urls) { + logger.info(String.format("%s/nifi-registry", url)); + } + } + } + + public void stop() { + try { + server.stop(); + } catch (Exception ex) { + logger.warn("Failed to stop web server", ex); + } + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml b/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml new file mode 100644 index 0000000..814dbd8 --- /dev/null +++ b/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml @@ -0,0 +1,556 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + 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. +--> +<web-app + xmlns="http://xmlns.jcp.org/xml/ns/javaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" + metadata-complete="false" + version="3.1"> + + <!-- ===================================================================== --> + <!-- This file contains the default descriptor for web applications. --> + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + <!-- The intent of this descriptor is to include jetty specific or common --> + <!-- configuration for all webapps. If a context has a webdefault.xml --> + <!-- descriptor, it is applied before the contexts own web.xml file --> + <!-- --> + <!-- A context may be assigned a default descriptor by: --> + <!-- + Calling WebApplicationContext.setDefaultsDescriptor --> + <!-- + Passed an arg to addWebApplications --> + <!-- --> + <!-- This file is used both as the resource within the jetty.jar (which is --> + <!-- used as the default if no explicit defaults descriptor is set) and it --> + <!-- is copied to the etc directory of the Jetty distro and explicitly --> + <!-- by the jetty.xml file. --> + <!-- --> + <!-- ===================================================================== --> + + <description> + Default web.xml file. + This file is applied to a Web application before it's own WEB_INF/web.xml file + </description> + + <!-- ==================================================================== --> + <!-- Removes static references to beans from javax.el.BeanELResolver to --> + <!-- ensure webapp classloader can be released on undeploy --> + <!-- ==================================================================== --> + <listener> + <listener-class>org.eclipse.jetty.servlet.listener.ELContextCleaner</listener-class> + </listener> + + <!-- ==================================================================== --> + <!-- Removes static cache of Methods from java.beans.Introspector to --> + <!-- ensure webapp classloader can be released on undeploy --> + <!-- ==================================================================== --> + <listener> + <listener-class>org.eclipse.jetty.servlet.listener.IntrospectorCleaner</listener-class> + </listener> + + + <!-- ==================================================================== --> + <!-- Context params to control Session Cookies --> + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + <!-- + UNCOMMENT TO ACTIVATE + <context-param> + <param-name>org.eclipse.jetty.servlet.SessionDomain</param-name> + <param-value>127.0.0.1</param-value> + </context-param> + <context-param> + <param-name>org.eclipse.jetty.servlet.SessionPath</param-name> + <param-value>/</param-value> + </context-param> + <context-param> + <param-name>org.eclipse.jetty.servlet.MaxAge</param-name> + <param-value>-1</param-value> + </context-param> + --> + + <!-- ==================================================================== --> + <!-- The default servlet. --> + <!-- This servlet, normally mapped to /, provides the handling for static --> + <!-- content, OPTIONS and TRACE methods for the context. --> + <!-- The following initParameters are supported: --> + <!-- + * acceptRanges If true, range requests and responses are + * supported + * + * dirAllowed If true, directory listings are returned if no + * welcome file is found. Else 403 Forbidden. + * + * welcomeServlets If true, attempt to dispatch to welcome files + * that are servlets, but only after no matching static + * resources could be found. If false, then a welcome + * file must exist on disk. If "exact", then exact + * servlet matches are supported without an existing file. + * Default is true. + * + * This must be false if you want directory listings, + * but have index.jsp in your welcome file list. + * + * redirectWelcome If true, welcome files are redirected rather than + * forwarded to. + * + * gzip If set to true, then static content will be served as + * gzip content encoded if a matching resource is + * found ending with ".gz" + * + * resourceBase Set to replace the context resource base + * + * resourceCache If set, this is a context attribute name, which the servlet + * will use to look for a shared ResourceCache instance. + * + * relativeResourceBase + * Set with a pathname relative to the base of the + * servlet context root. Useful for only serving static content out + * of only specific subdirectories. + * + * pathInfoOnly If true, only the path info will be applied to the resourceBase + * + * stylesheet Set with the location of an optional stylesheet that will be used + * to decorate the directory listing html. + * + * aliases If True, aliases of resources are allowed (eg. symbolic + * links and caps variations). May bypass security constraints. + * + * etags If True, weak etags will be generated and handled. + * + * maxCacheSize The maximum total size of the cache or 0 for no cache. + * maxCachedFileSize The maximum size of a file to cache + * maxCachedFiles The maximum number of files to cache + * + * useFileMappedBuffer + * If set to true, it will use mapped file buffer to serve static content + * when using NIO connector. Setting this value to false means that + * a direct buffer will be used instead of a mapped file buffer. + * By default, this is set to true. + * + * cacheControl If set, all static content will have this value set as the cache-control + * header. + * + --> + + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + <servlet> + <servlet-name>default</servlet-name> + <servlet-class>org.eclipse.jetty.servlet.DefaultServlet</servlet-class> + <init-param> + <param-name>aliases</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>acceptRanges</param-name> + <param-value>true</param-value> + </init-param> + <init-param> + <param-name>dirAllowed</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>welcomeServlets</param-name> + <param-value>true</param-value> + </init-param> + <init-param> + <param-name>redirectWelcome</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>maxCacheSize</param-name> + <param-value>256000000</param-value> + </init-param> + <init-param> + <param-name>maxCachedFileSize</param-name> + <param-value>200000000</param-value> + </init-param> + <init-param> + <param-name>maxCachedFiles</param-name> + <param-value>2048</param-value> + </init-param> + <init-param> + <param-name>gzip</param-name> + <param-value>true</param-value> + </init-param> + <init-param> + <param-name>etags</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>useFileMappedBuffer</param-name> + <param-value>true</param-value> + </init-param> + <!-- + <init-param> + <param-name>resourceCache</param-name> + <param-value>resourceCache</param-value> + </init-param> + --> + <!-- + <init-param> + <param-name>cacheControl</param-name> + <param-value>max-age=3600,public</param-value> + </init-param> + --> + <load-on-startup>0</load-on-startup> + </servlet> + + <servlet-mapping> + <servlet-name>default</servlet-name> + <url-pattern>/</url-pattern> + </servlet-mapping> + + + <!-- ==================================================================== --> + <!-- JSP Servlet --> + <!-- This is the jasper JSP servlet from the jakarta project --> + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + <!-- The JSP page compiler and execution servlet, which is the mechanism --> + <!-- used by Glassfish to support JSP pages. Traditionally, this servlet --> + <!-- is mapped to URL patterh "*.jsp". This servlet supports the --> + <!-- following initialization parameters (default values are in square --> + <!-- brackets): --> + <!-- --> + <!-- checkInterval If development is false and reloading is true, --> + <!-- background compiles are enabled. checkInterval --> + <!-- is the time in seconds between checks to see --> + <!-- if a JSP page needs to be recompiled. [300] --> + <!-- --> + <!-- compiler Which compiler Ant should use to compile JSP --> + <!-- pages. See the Ant documenation for more --> + <!-- information. [javac] --> + <!-- --> + <!-- classdebuginfo Should the class file be compiled with --> + <!-- debugging information? [true] --> + <!-- --> + <!-- classpath What class path should I use while compiling --> + <!-- generated servlets? [Created dynamically --> + <!-- based on the current web application] --> + <!-- Set to ? to make the container explicitly set --> + <!-- this parameter. --> + <!-- --> + <!-- development Is Jasper used in development mode (will check --> + <!-- for JSP modification on every access)? [true] --> + <!-- --> + <!-- enablePooling Determines whether tag handler pooling is --> + <!-- enabled [true] --> + <!-- --> + <!-- fork Tell Ant to fork compiles of JSP pages so that --> + <!-- a separate JVM is used for JSP page compiles --> + <!-- from the one Tomcat is running in. [true] --> + <!-- --> + <!-- ieClassId The class-id value to be sent to Internet --> + <!-- Explorer when using <jsp:plugin> tags. --> + <!-- [clsid:8AD9C840-044E-11D1-B3E9-00805F499D93] --> + <!-- --> + <!-- javaEncoding Java file encoding to use for generating java --> + <!-- source files. [UTF-8] --> + <!-- --> + <!-- keepgenerated Should we keep the generated Java source code --> + <!-- for each page instead of deleting it? [true] --> + <!-- --> + <!-- logVerbosityLevel The level of detailed messages to be produced --> + <!-- by this servlet. Increasing levels cause the --> + <!-- generation of more messages. Valid values are --> + <!-- FATAL, ERROR, WARNING, INFORMATION, and DEBUG. --> + <!-- [WARNING] --> + <!-- --> + <!-- mappedfile Should we generate static content with one --> + <!-- print statement per input line, to ease --> + <!-- debugging? [false] --> + <!-- --> + <!-- --> + <!-- reloading Should Jasper check for modified JSPs? [true] --> + <!-- --> + <!-- suppressSmap Should the generation of SMAP info for JSR45 --> + <!-- debugging be suppressed? [false] --> + <!-- --> + <!-- dumpSmap Should the SMAP info for JSR45 debugging be --> + <!-- dumped to a file? [false] --> + <!-- False if suppressSmap is true --> + <!-- --> + <!-- scratchdir What scratch directory should we use when --> + <!-- compiling JSP pages? [default work directory --> + <!-- for the current web application] --> + <!-- --> + <!-- tagpoolMaxSize The maximum tag handler pool size [5] --> + <!-- --> + <!-- xpoweredBy Determines whether X-Powered-By response --> + <!-- header is added by generated servlet [false] --> + <!-- --> + <!-- If you wish to use Jikes to compile JSP pages: --> + <!-- Set the init parameter "compiler" to "jikes". Define --> + <!-- the property "-Dbuild.compiler.emacs=true" when starting Jetty --> + <!-- to cause Jikes to emit error messages in a format compatible with --> + <!-- Jasper. --> + <!-- If you get an error reporting that jikes can't use UTF-8 encoding, --> + <!-- try setting the init parameter "javaEncoding" to "ISO-8859-1". --> + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + <servlet id="jsp"> + <servlet-name>jsp</servlet-name> + <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> + <init-param> + <param-name>logVerbosityLevel</param-name> + <param-value>DEBUG</param-value> + </init-param> + <init-param> + <param-name>fork</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>keepgenerated</param-name> + <param-value>true</param-value> + </init-param> + <init-param> + <param-name>development</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>xpoweredBy</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>compilerTargetVM</param-name> + <param-value>1.7</param-value> + </init-param> + <init-param> + <param-name>compilerSourceVM</param-name> + <param-value>1.7</param-value> + </init-param> + <!-- + <init-param> + <param-name>classpath</param-name> + <param-value>?</param-value> + </init-param> + --> + <load-on-startup>0</load-on-startup> + </servlet> + + <servlet-mapping> + <servlet-name>jsp</servlet-name> + <url-pattern>*.jsp</url-pattern> + <url-pattern>*.jspf</url-pattern> + <url-pattern>*.jspx</url-pattern> + <url-pattern>*.xsp</url-pattern> + <url-pattern>*.JSP</url-pattern> + <url-pattern>*.JSPF</url-pattern> + <url-pattern>*.JSPX</url-pattern> + <url-pattern>*.XSP</url-pattern> + </servlet-mapping> + + + <!-- ==================================================================== --> + <session-config> + <session-timeout>30</session-timeout> + </session-config> + + <!-- ==================================================================== --> + <!-- Default MIME mappings --> + <!-- The default MIME mappings are provided by the mime.properties --> + <!-- resource in the org.eclipse.jetty.server.jar file. Additional or modified --> + <!-- mappings may be specified here --> + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + <!-- UNCOMMENT TO ACTIVATE + <mime-mapping> + <extension>mysuffix</extension> + <mime-type>mymime/type</mime-type> + </mime-mapping> + --> + + <!-- ==================================================================== --> + <welcome-file-list> + <welcome-file>index.html</welcome-file> + <welcome-file>index.htm</welcome-file> + <welcome-file>index.jsp</welcome-file> + </welcome-file-list> + + <!-- ==================================================================== --> + <locale-encoding-mapping-list> + <locale-encoding-mapping> + <locale>ar</locale> + <encoding>ISO-8859-6</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>be</locale> + <encoding>ISO-8859-5</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>bg</locale> + <encoding>ISO-8859-5</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>ca</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>cs</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>da</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>de</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>el</locale> + <encoding>ISO-8859-7</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>en</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>es</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>et</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>fi</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>fr</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>hr</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>hu</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>is</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>it</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>iw</locale> + <encoding>ISO-8859-8</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>ja</locale> + <encoding>Shift_JIS</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>ko</locale> + <encoding>EUC-KR</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>lt</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>lv</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>mk</locale> + <encoding>ISO-8859-5</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>nl</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>no</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>pl</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>pt</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>ro</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>ru</locale> + <encoding>ISO-8859-5</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>sh</locale> + <encoding>ISO-8859-5</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>sk</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>sl</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>sq</locale> + <encoding>ISO-8859-2</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>sr</locale> + <encoding>ISO-8859-5</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>sv</locale> + <encoding>ISO-8859-1</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>tr</locale> + <encoding>ISO-8859-9</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>uk</locale> + <encoding>ISO-8859-5</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>zh</locale> + <encoding>GB2312</encoding> + </locale-encoding-mapping> + <locale-encoding-mapping> + <locale>zh_TW</locale> + <encoding>Big5</encoding> + </locale-encoding-mapping> + </locale-encoding-mapping-list> + + <security-constraint> + <web-resource-collection> + <web-resource-name>Disable TRACE</web-resource-name> + <url-pattern>/</url-pattern> + <http-method>TRACE</http-method> + </web-resource-collection> + <auth-constraint/> + </security-constraint> + <security-constraint> + <web-resource-collection> + <web-resource-name>Enable everything but TRACE</web-resource-name> + <url-pattern>/</url-pattern> + <http-method-omission>TRACE</http-method-omission> + </web-resource-collection> + </security-constraint> + +</web-app> + http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/pom.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-properties/pom.xml b/nifi-registry-core/nifi-registry-properties/pom.xml new file mode 100644 index 0000000..a6d6422 --- /dev/null +++ b/nifi-registry-core/nifi-registry-properties/pom.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.nifi.registry</groupId> + <artifactId>nifi-registry-core</artifactId> + <version>0.3.0-SNAPSHOT</version> + </parent> + <artifactId>nifi-registry-properties</artifactId> + <packaging>jar</packaging> + + <build> + <plugins> + <plugin> + <groupId>org.codehaus.gmavenplus</groupId> + <artifactId>gmavenplus-plugin</artifactId> + <version>1.5</version> + <executions> + <execution> + <goals> + <goal>addTestSources</goal> + <goal>testCompile</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <version>1.55</version> + </dependency> + <dependency> + <groupId>org.codehaus.groovy</groupId> + <artifactId>groovy-all</artifactId> + <version>2.4.12</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>cglib</groupId> + <artifactId>cglib-nodep</artifactId> + <version>2.2.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>1.7.12</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java new file mode 100644 index 0000000..b7d1d2e --- /dev/null +++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java @@ -0,0 +1,265 @@ +/* + * 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.registry.properties; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.DecoderException; +import org.bouncycastle.util.encoders.EncoderException; +import org.bouncycastle.util.encoders.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class AESSensitivePropertyProvider implements SensitivePropertyProvider { + private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProvider.class); + + private static final String IMPLEMENTATION_NAME = "AES Sensitive Property Provider"; + private static final String IMPLEMENTATION_KEY = "aes/gcm/"; + private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final String PROVIDER = "BC"; + private static final String DELIMITER = "||"; // "|" is not a valid Base64 character, so ensured not to be present in cipher text + private static final int IV_LENGTH = 12; + private static final int MIN_CIPHER_TEXT_LENGTH = IV_LENGTH * 4 / 3 + DELIMITER.length() + 1; + + private Cipher cipher; + private final SecretKey key; + + public AESSensitivePropertyProvider(String keyHex) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { + byte[] key = validateKey(keyHex); + + try { + Security.addProvider(new BouncyCastleProvider()); + cipher = Cipher.getInstance(ALGORITHM, PROVIDER); + // Only store the key if the cipher was initialized successfully + this.key = new SecretKeySpec(key, "AES"); + } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) { + logger.error("Encountered an error initializing the {}: {}", IMPLEMENTATION_NAME, e.getMessage()); + throw new SensitivePropertyProtectionException("Error initializing the protection cipher", e); + } + } + + private byte[] validateKey(String keyHex) { + if (keyHex == null || StringUtils.isBlank(keyHex)) { + throw new SensitivePropertyProtectionException("The key cannot be empty"); + } + keyHex = formatHexKey(keyHex); + if (!isHexKeyValid(keyHex)) { + throw new SensitivePropertyProtectionException("The key must be a valid hexadecimal key"); + } + byte[] key = Hex.decode(keyHex); + final List<Integer> validKeyLengths = getValidKeyLengths(); + if (!validKeyLengths.contains(key.length * 8)) { + List<String> validKeyLengthsAsStrings = validKeyLengths.stream().map(i -> Integer.toString(i)).collect(Collectors.toList()); + throw new SensitivePropertyProtectionException("The key (" + key.length * 8 + " bits) must be a valid length: " + StringUtils.join(validKeyLengthsAsStrings, ", ")); + } + return key; + } + + public AESSensitivePropertyProvider(byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { + this(key == null ? "" : Hex.toHexString(key)); + } + + private static String formatHexKey(String input) { + if (input == null || StringUtils.isBlank(input)) { + return ""; + } + return input.replaceAll("[^0-9a-fA-F]", "").toLowerCase(); + } + + private static boolean isHexKeyValid(String key) { + if (key == null || StringUtils.isBlank(key)) { + return false; + } + // Key length is in "nibbles" (i.e. one hex char = 4 bits) + return getValidKeyLengths().contains(key.length() * 4) && key.matches("^[0-9a-fA-F]*$"); + } + + private static List<Integer> getValidKeyLengths() { + List<Integer> validLengths = new ArrayList<>(); + validLengths.add(128); + + try { + if (Cipher.getMaxAllowedKeyLength("AES") > 128) { + validLengths.add(192); + validLengths.add(256); + } else { + logger.warn("JCE Unlimited Strength Cryptography Jurisdiction policies are not available, so the max key length is 128 bits"); + } + } catch (NoSuchAlgorithmException e) { + logger.warn("Encountered an error determining the max key length", e); + } + + return validLengths; + } + + /** + * Returns the name of the underlying implementation. + * + * @return the name of this sensitive property provider + */ + @Override + public String getName() { + return IMPLEMENTATION_NAME; + } + + /** + * Returns the key used to identify the provider implementation in {@code nifi.properties}. + * + * @return the key to persist in the sibling property + */ + @Override + public String getIdentifierKey() { + return IMPLEMENTATION_KEY + getKeySize(Hex.toHexString(key.getEncoded())); + } + + private int getKeySize(String key) { + if (StringUtils.isBlank(key)) { + return 0; + } else { + // A key in hexadecimal format has one char per nibble (4 bits) + return formatHexKey(key).length() * 4; + } + } + + /** + * Returns the encrypted cipher text. + * + * @param unprotectedValue the sensitive value + * @return the value to persist in the {@code nifi.properties} file + * @throws SensitivePropertyProtectionException if there is an exception encrypting the value + */ + @Override + public String protect(String unprotectedValue) throws SensitivePropertyProtectionException { + if (unprotectedValue == null || unprotectedValue.trim().length() == 0) { + throw new IllegalArgumentException("Cannot encrypt an empty value"); + } + + // Generate IV + byte[] iv = generateIV(); + if (iv.length < IV_LENGTH) { + throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes"); + } + + try { + // Initialize cipher for encryption + cipher.init(Cipher.ENCRYPT_MODE, this.key, new IvParameterSpec(iv)); + + byte[] plainBytes = unprotectedValue.getBytes(StandardCharsets.UTF_8); + byte[] cipherBytes = cipher.doFinal(plainBytes); + logger.info(getName() + " encrypted a sensitive value successfully"); + return base64Encode(iv) + DELIMITER + base64Encode(cipherBytes); + // return Base64.toBase64String(iv) + DELIMITER + Base64.toBase64String(cipherBytes); + } catch (BadPaddingException | IllegalBlockSizeException | EncoderException | InvalidAlgorithmParameterException | InvalidKeyException e) { + final String msg = "Error encrypting a protected value"; + logger.error(msg, e); + throw new SensitivePropertyProtectionException(msg, e); + } + } + + private String base64Encode(byte[] input) { + return Base64.toBase64String(input).replaceAll("=", ""); + } + + /** + * Generates a new random IV of 12 bytes using {@link SecureRandom}. + * + * @return the IV + */ + private byte[] generateIV() { + byte[] iv = new byte[IV_LENGTH]; + new SecureRandom().nextBytes(iv); + return iv; + } + + /** + * Returns the decrypted plaintext. + * + * @param protectedValue the cipher text read from the {@code nifi.properties} file + * @return the raw value to be used by the application + * @throws SensitivePropertyProtectionException if there is an error decrypting the cipher text + */ + @Override + public String unprotect(String protectedValue) throws SensitivePropertyProtectionException { + if (protectedValue == null || protectedValue.trim().length() < MIN_CIPHER_TEXT_LENGTH) { + throw new IllegalArgumentException("Cannot decrypt a cipher text shorter than " + MIN_CIPHER_TEXT_LENGTH + " chars"); + } + + if (!protectedValue.contains(DELIMITER)) { + throw new IllegalArgumentException("The cipher text does not contain the delimiter " + DELIMITER + " -- it should be of the form Base64(IV) || Base64(cipherText)"); + } + + protectedValue = protectedValue.trim(); + + final String IV_B64 = protectedValue.substring(0, protectedValue.indexOf(DELIMITER)); + byte[] iv = Base64.decode(IV_B64); + if (iv.length < IV_LENGTH) { + throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes"); + } + + String CIPHERTEXT_B64 = protectedValue.substring(protectedValue.indexOf(DELIMITER) + 2); + + // Restore the = padding if necessary to reconstitute the GCM MAC check + if (CIPHERTEXT_B64.length() % 4 != 0) { + final int paddedLength = CIPHERTEXT_B64.length() + 4 - (CIPHERTEXT_B64.length() % 4); + CIPHERTEXT_B64 = StringUtils.rightPad(CIPHERTEXT_B64, paddedLength, '='); + } + + try { + byte[] cipherBytes = Base64.decode(CIPHERTEXT_B64); + + cipher.init(Cipher.DECRYPT_MODE, this.key, new IvParameterSpec(iv)); + byte[] plainBytes = cipher.doFinal(cipherBytes); + logger.debug(getName() + " decrypted a sensitive value successfully"); + return new String(plainBytes, StandardCharsets.UTF_8); + } catch (BadPaddingException | IllegalBlockSizeException | DecoderException | InvalidAlgorithmParameterException | InvalidKeyException e) { + final String msg = "Error decrypting a protected value"; + logger.error(msg, e); + throw new SensitivePropertyProtectionException(msg, e); + } + } + + public static int getIvLength() { + return IV_LENGTH; + } + + public static int getMinCipherTextLength() { + return MIN_CIPHER_TEXT_LENGTH; + } + + public static String getDelimiter() { + return DELIMITER; + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java new file mode 100644 index 0000000..5c24a73 --- /dev/null +++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java @@ -0,0 +1,54 @@ +/* + * 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.registry.properties; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.NoSuchPaddingException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public class AESSensitivePropertyProviderFactory implements SensitivePropertyProviderFactory { + private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactory.class); + + private String keyHex; + + public AESSensitivePropertyProviderFactory(String keyHex) { + this.keyHex = keyHex; + } + + public SensitivePropertyProvider getProvider() throws SensitivePropertyProtectionException { + try { + if (keyHex != null && !StringUtils.isBlank(keyHex)) { + return new AESSensitivePropertyProvider(keyHex); + } else { + throw new SensitivePropertyProtectionException("The provider factory cannot generate providers without a key"); + } + } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) { + String msg = "Error creating AES Sensitive Property Provider"; + logger.warn(msg, e); + throw new SensitivePropertyProtectionException(msg, e); + } + } + + @Override + public String toString() { + return "SensitivePropertyProviderFactory for creating AESSensitivePropertyProviders"; + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java new file mode 100644 index 0000000..df4047f --- /dev/null +++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java @@ -0,0 +1,129 @@ +/* + * 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.registry.properties; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class MultipleSensitivePropertyProtectionException extends SensitivePropertyProtectionException { + + private Set<String> failedKeys; + + /** + * Constructs a new throwable with {@code null} as its detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * <p> + * <p>The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + */ + public MultipleSensitivePropertyProtectionException() { + } + + /** + * Constructs a new throwable with the specified detail message. The + * cause is not initialized, and may subsequently be initialized by + * a call to {@link #initCause}. + * <p> + * <p>The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public MultipleSensitivePropertyProtectionException(String message) { + super(message); + } + + /** + * Constructs a new throwable with the specified detail message and + * cause. <p>Note that the detail message associated with + * {@code cause} is <i>not</i> automatically incorporated in + * this throwable's detail message. + * <p> + * <p>The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public MultipleSensitivePropertyProtectionException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new throwable with the specified cause and a detail + * message of {@code (cause==null ? null : cause.toString())} (which + * typically contains the class and detail message of {@code cause}). + * This constructor is useful for throwables that are little more than + * wrappers for other throwables (for example, PrivilegedActionException). + * <p> + * <p>The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public MultipleSensitivePropertyProtectionException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the provided message and a unique set of the keys that caused the error. + * + * @param message the message + * @param failedKeys any failed keys + */ + public MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys) { + this(message, failedKeys, null); + } + + /** + * Constructs a new exception with the provided message and a unique set of the keys that caused the error. + * + * @param message the message + * @param failedKeys any failed keys + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys, Throwable cause) { + super(message, cause); + this.failedKeys = new HashSet<>(failedKeys); + } + + public Set<String> getFailedKeys() { + return this.failedKeys; + } + + @Override + public String toString() { + return "SensitivePropertyProtectionException for [" + StringUtils.join(this.failedKeys, ", ") + "]: " + getLocalizedMessage(); + } +}