NIFI-4501

- Changed request header handling logic.
- Removed unnecessary Maven dependency.
- This closes #2279
This commit is contained in:
Andy LoPresto 2017-10-31 17:00:03 -07:00 committed by Matt Gilman
parent c89d793364
commit 5d643edfab
No known key found for this signature in database
GPG Key ID: DF61EC19432AEE37
17 changed files with 1163 additions and 184 deletions

View File

@ -177,6 +177,7 @@ public abstract class NiFiProperties {
public static final String WEB_WORKING_DIR = "nifi.web.jetty.working.directory"; public static final String WEB_WORKING_DIR = "nifi.web.jetty.working.directory";
public static final String WEB_THREADS = "nifi.web.jetty.threads"; public static final String WEB_THREADS = "nifi.web.jetty.threads";
public static final String WEB_MAX_HEADER_SIZE = "nifi.web.max.header.size"; public static final String WEB_MAX_HEADER_SIZE = "nifi.web.max.header.size";
public static final String WEB_PROXY_CONTEXT_PATH = "nifi.web.proxy.context.path";
// ui properties // ui properties
public static final String UI_BANNER_TEXT = "nifi.ui.banner.text"; public static final String UI_BANNER_TEXT = "nifi.ui.banner.text";
@ -256,8 +257,8 @@ public abstract class NiFiProperties {
public static final String DEFAULT_ZOOKEEPER_SESSION_TIMEOUT = "3 secs"; public static final String DEFAULT_ZOOKEEPER_SESSION_TIMEOUT = "3 secs";
public static final String DEFAULT_ZOOKEEPER_ROOT_NODE = "/nifi"; public static final String DEFAULT_ZOOKEEPER_ROOT_NODE = "/nifi";
public static final String DEFAULT_ZOOKEEPER_AUTH_TYPE = "default"; public static final String DEFAULT_ZOOKEEPER_AUTH_TYPE = "default";
public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_HOST_FROM_PRINCIPAL = "true"; public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_HOST_FROM_PRINCIPAL = "true";
public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_REALM_FROM_PRINCIPAL = "true"; public static final String DEFAULT_ZOOKEEPER_KERBEROS_REMOVE_REALM_FROM_PRINCIPAL = "true";
public static final String DEFAULT_SITE_TO_SITE_HTTP_TRANSACTION_TTL = "30 secs"; public static final String DEFAULT_SITE_TO_SITE_HTTP_TRANSACTION_TTL = "30 secs";
public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_ENABLED = "true"; public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_ENABLED = "true";
public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_TIME = "30 days"; public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_TIME = "30 days";
@ -1084,7 +1085,7 @@ public abstract class NiFiProperties {
* Returns the number of claims to keep open for writing. Ideally, this will be at * Returns the number of claims to keep open for writing. Ideally, this will be at
* least as large as the number of threads that will be updating the repository simultaneously but we don't want * least as large as the number of threads that will be updating the repository simultaneously but we don't want
* to get too large because it will hold open up to this many FileOutputStreams. * to get too large because it will hold open up to this many FileOutputStreams.
* * <p>
* Default is {@link #DEFAULT_MAX_FLOWFILES_PER_CLAIM} * Default is {@link #DEFAULT_MAX_FLOWFILES_PER_CLAIM}
* *
* @return the maximum number of flow files per claim * @return the maximum number of flow files per claim
@ -1100,7 +1101,7 @@ public abstract class NiFiProperties {
/** /**
* Returns the maximum size, in bytes, that claims should grow before writing a new file. This means that we won't continually write to one * Returns the maximum size, in bytes, that claims should grow before writing a new file. This means that we won't continually write to one
* file that keeps growing but gives us a chance to bunch together many small files. * file that keeps growing but gives us a chance to bunch together many small files.
* * <p>
* Default is {@link #DEFAULT_MAX_APPENDABLE_CLAIM_SIZE} * Default is {@link #DEFAULT_MAX_APPENDABLE_CLAIM_SIZE}
* *
* @return the maximum appendable claim size * @return the maximum appendable claim size
@ -1285,6 +1286,42 @@ public abstract class NiFiProperties {
return keys; return keys;
} }
/**
* Returns the whitelisted proxy context paths as a comma-delimited string. The paths have been normalized to the form {@code /some/context/path}.
*
* Note: Calling {@code NiFiProperties.getProperty(NiFiProperties.WEB_PROXY_CONTEXT_PATH)} will not normalize the paths.
*
* @return the path(s)
*/
public String getWhitelistedContextPaths() {
return StringUtils.join(getWhitelistedContextPathsAsList(), ",");
}
/**
* Returns the whitelisted proxy context paths as a list of paths. The paths have been normalized to the form {@code /some/context/path}.
*
* @return the path(s)
*/
public List<String> getWhitelistedContextPathsAsList() {
String rawProperty = getProperty(WEB_PROXY_CONTEXT_PATH, "");
List<String> contextPaths = Arrays.asList(rawProperty.split(","));
return contextPaths.stream()
.map(this::normalizeContextPath).collect(Collectors.toList());
}
private String normalizeContextPath(String cp) {
if (cp == null || cp.equalsIgnoreCase("")) {
return "";
} else {
String trimmedCP = cp.trim();
// Ensure it starts with a leading slash and does not end in a trailing slash
// There's a potential for the path to be something like bad/path/// but this is semi-trusted data from an admin-accessible file and there are way worse possibilities here
trimmedCP = trimmedCP.startsWith("/") ? trimmedCP : "/" + trimmedCP;
trimmedCP = trimmedCP.endsWith("/") ? trimmedCP.substring(0, trimmedCP.length() - 1) : trimmedCP;
return trimmedCP;
}
}
private List<String> getProvenanceRepositoryEncryptionKeyProperties() { private List<String> getProvenanceRepositoryEncryptionKeyProperties() {
// Filter all the property keys that define a key // Filter all the property keys that define a key
return getPropertyKeys().stream().filter(k -> return getPropertyKeys().stream().filter(k ->

View File

@ -16,28 +16,31 @@
*/ */
package org.apache.nifi.web.util; package org.apache.nifi.web.util;
import java.net.URI;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.UriBuilderException;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.security.util.CertificateUtils; import org.apache.nifi.security.util.CertificateUtils;
import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/** /**
* Common utilities related to web development. * Common utilities related to web development.
*
*/ */
public final class WebUtils { public final class WebUtils {
@ -45,6 +48,9 @@ public final class WebUtils {
final static ReadWriteLock lock = new ReentrantReadWriteLock(); final static ReadWriteLock lock = new ReentrantReadWriteLock();
private static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
private static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
private WebUtils() { private WebUtils() {
} }
@ -54,7 +60,6 @@ public final class WebUtils {
* automatically configured for JSON serialization/deserialization. * automatically configured for JSON serialization/deserialization.
* *
* @param config client configuration * @param config client configuration
*
* @return a Client instance * @return a Client instance
*/ */
public static Client createClient(final ClientConfig config) { public static Client createClient(final ClientConfig config) {
@ -67,8 +72,7 @@ public final class WebUtils {
* will be automatically configured for JSON serialization/deserialization. * will be automatically configured for JSON serialization/deserialization.
* *
* @param config client configuration * @param config client configuration
* @param ctx security context * @param ctx security context
*
* @return a Client instance * @return a Client instance
*/ */
public static Client createClient(final ClientConfig config, final SSLContext ctx) { public static Client createClient(final ClientConfig config, final SSLContext ctx) {
@ -81,9 +85,8 @@ public final class WebUtils {
* will be automatically configured for JSON serialization/deserialization. * will be automatically configured for JSON serialization/deserialization.
* *
* @param config client configuration * @param config client configuration
* @param ctx security context, which may be null for non-secure client * @param ctx security context, which may be null for non-secure client
* creation * creation
*
* @return a Client instance * @return a Client instance
*/ */
private static Client createClientHelper(final ClientConfig config, final SSLContext ctx) { private static Client createClientHelper(final ClientConfig config, final SSLContext ctx) {
@ -128,4 +131,119 @@ public final class WebUtils {
} }
/**
* This method will check the provided context path headers against a whitelist (provided in nifi.properties) and throw an exception if the requested context path is not registered.
*
* @param uri the request URI
* @param request the HTTP request
* @param whitelistedContextPaths comma-separated list of valid context paths
* @return the resource path
* @throws UriBuilderException if the requested context path is not registered (header poisoning)
*/
public static String getResourcePath(URI uri, HttpServletRequest request, String whitelistedContextPaths) throws UriBuilderException {
String resourcePath = uri.getPath();
// Determine and normalize the context path
String determinedContextPath = determineContextPath(request);
determinedContextPath = normalizeContextPath(determinedContextPath);
// If present, check it and prepend to the resource path
if (StringUtils.isNotBlank(determinedContextPath)) {
verifyContextPath(whitelistedContextPaths, determinedContextPath);
// Determine the complete resource path
resourcePath = determinedContextPath + resourcePath;
}
return resourcePath;
}
/**
* Throws an exception if the provided context path is not in the whitelisted context paths list.
*
* @param whitelistedContextPaths a comma-delimited list of valid context paths
* @param determinedContextPath the normalized context path from a header
* @throws UriBuilderException if the context path is not safe
*/
public static void verifyContextPath(String whitelistedContextPaths, String determinedContextPath) throws UriBuilderException {
// If blank, ignore
if (StringUtils.isBlank(determinedContextPath)) {
return;
}
// Check it against the whitelist
List<String> individualContextPaths = Arrays.asList(StringUtils.split(whitelistedContextPaths, ","));
if (!individualContextPaths.contains(determinedContextPath)) {
final String msg = "The provided context path [" + determinedContextPath + "] was not whitelisted [" + whitelistedContextPaths + "]";
logger.error(msg);
throw new UriBuilderException(msg);
}
}
/**
* Returns a normalized context path (leading /, no trailing /). If the parameter is blank, an empty string will be returned.
*
* @param determinedContextPath the raw context path
* @return the normalized context path
*/
public static String normalizeContextPath(String determinedContextPath) {
if (StringUtils.isNotBlank(determinedContextPath)) {
// normalize context path
if (!determinedContextPath.startsWith("/")) {
determinedContextPath = "/" + determinedContextPath;
}
if (determinedContextPath.endsWith("/")) {
determinedContextPath = determinedContextPath.substring(0, determinedContextPath.length() - 1);
}
return determinedContextPath;
} else {
return "";
}
}
/**
* Determines the context path if populated in {@code X-ProxyContextPath} or {@code X-ForwardContext} headers. If not populated, returns an empty string.
*
* @param request the HTTP request
* @return the provided context path or an empty string
*/
public static String determineContextPath(HttpServletRequest request) {
String contextPath = request.getContextPath();
String proxyContextPath = request.getHeader(PROXY_CONTEXT_PATH_HTTP_HEADER);
String forwardedContext = request.getHeader(FORWARDED_CONTEXT_HTTP_HEADER);
logger.debug("Context path: " + contextPath);
String determinedContextPath = "";
// If either header is set, log both
if (anyNotBlank(proxyContextPath, forwardedContext)) {
logger.debug(String.format("On the request, the following context paths were parsed" +
" from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s",
proxyContextPath, forwardedContext));
// Implementing preferred order here: PCP, FCP
determinedContextPath = StringUtils.isNotBlank(proxyContextPath) ? proxyContextPath : forwardedContext;
}
logger.debug("Determined context path: " + determinedContextPath);
return determinedContextPath;
}
/**
* Returns true if any of the provided arguments are not blank.
*
* @param strings a variable number of strings
* @return true if any string has content (not empty or all whitespace)
*/
private static boolean anyNotBlank(String... strings) {
for (String s : strings) {
if (StringUtils.isNotBlank(s)) {
return true;
}
}
return false;
}
} }

View File

@ -0,0 +1,275 @@
/*
* 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.util
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.UriBuilderException
@RunWith(JUnit4.class)
class WebUtilsTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(WebUtilsTest.class)
static final String PCP_HEADER = "X-ProxyContextPath"
static final String FC_HEADER = "X-Forwarded-Context"
static final String WHITELISTED_PATH = "/some/context/path"
@BeforeClass
static void setUpOnce() throws Exception {
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
}
HttpServletRequest mockRequest(Map keys) {
HttpServletRequest mockRequest = [
getContextPath: { ->
logger.mock("Request.getContextPath() -> default/path")
"default/path"
},
getHeader : { String k ->
logger.mock("Request.getHeader($k) -> ${keys}")
switch (k) {
case PCP_HEADER:
return keys["proxy"]
break
case FC_HEADER:
return keys["forward"]
break
default:
return ""
}
}] as HttpServletRequest
mockRequest
}
@Test
void testShouldDetermineCorrectContextPathWhenPresent() throws Exception {
// Arrange
final String CORRECT_CONTEXT_PATH = WHITELISTED_PATH
final String WRONG_CONTEXT_PATH = "this/is/a/bad/path"
// Variety of requests with different ordering of context paths (the correct one is always "some/context/path"
HttpServletRequest proxyRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH])
HttpServletRequest forwardedRequest = mockRequest([forward: CORRECT_CONTEXT_PATH])
HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH, forward: WRONG_CONTEXT_PATH])
List<HttpServletRequest> requests = [proxyRequest, forwardedRequest, proxyBeforeForwardedRequest]
// Act
requests.each { HttpServletRequest request ->
String determinedContextPath = WebUtils.determineContextPath(request)
logger.info("Determined context path: ${determinedContextPath}")
// Assert
assert determinedContextPath == CORRECT_CONTEXT_PATH
}
}
@Test
void testShouldDetermineCorrectContextPathWhenAbsent() throws Exception {
// Arrange
final String CORRECT_CONTEXT_PATH = ""
// Variety of requests with different ordering of non-existent context paths (the correct one is always ""
HttpServletRequest proxyRequest = mockRequest([proxy: ""])
HttpServletRequest proxySpacesRequest = mockRequest([proxy: " "])
HttpServletRequest forwardedRequest = mockRequest([forward: ""])
HttpServletRequest forwardedSpacesRequest = mockRequest([forward: " "])
HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: "", forward: ""])
List<HttpServletRequest> requests = [proxyRequest, proxySpacesRequest, forwardedRequest, forwardedSpacesRequest, proxyBeforeForwardedRequest]
// Act
requests.each { HttpServletRequest request ->
String determinedContextPath = WebUtils.determineContextPath(request)
logger.info("Determined context path: ${determinedContextPath}")
// Assert
assert determinedContextPath == CORRECT_CONTEXT_PATH
}
}
@Test
void testShouldNormalizeContextPath() throws Exception {
// Arrange
final String CORRECT_CONTEXT_PATH = WHITELISTED_PATH
final String TRIMMED_PATH = WHITELISTED_PATH[1..-1] // Trims leading /
// Variety of different context paths (the correct one is always "/some/context/path")
List<String> contextPaths = ["/$TRIMMED_PATH", "/" + TRIMMED_PATH, TRIMMED_PATH, TRIMMED_PATH + "/"]
// Act
contextPaths.each { String contextPath ->
String normalizedContextPath = WebUtils.normalizeContextPath(contextPath)
logger.info("Normalized context path: ${normalizedContextPath} <- ${contextPath}")
// Assert
assert normalizedContextPath == CORRECT_CONTEXT_PATH
}
}
@Test
void testGetResourcePathShouldBlockContextPathHeaderIfNotInWhitelist() throws Exception {
// Arrange
logger.info("Whitelisted path(s): ")
HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "any/context/path"])
HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "any/context/path", forward: "any/other/context/path"])
List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader]
// Act
requests.each { HttpServletRequest request ->
def msg = shouldFail(UriBuilderException) {
String generatedResourcePath = WebUtils.getResourcePath(new URI('https://nifi.apache.org/actualResource'), request, "")
logger.unexpected("Generated Resource Path: ${generatedResourcePath}")
}
// Assert
logger.expected(msg)
assert msg =~ "The provided context path \\[.*\\] was not whitelisted \\[\\]"
}
}
@Test
void testGetResourcePathShouldAllowContextPathHeaderIfInWhitelist() throws Exception {
// Arrange
logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
HttpServletRequest requestWithForwardHeader = mockRequest([forward: "some/context/path"])
HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path"])
List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader]
// Act
requests.each { HttpServletRequest request ->
String generatedResourcePath = WebUtils.getResourcePath(new URI('https://nifi.apache.org/actualResource'), request, WHITELISTED_PATH)
logger.info("Generated Resource Path: ${generatedResourcePath}")
// Assert
assert generatedResourcePath == "${WHITELISTED_PATH}/actualResource"
}
}
@Test
void testGetResourcePathShouldAllowContextPathHeaderIfElementInMultipleWhitelist() throws Exception {
// Arrange
String multipleWhitelistedPaths = [WHITELISTED_PATH, "/another/path", "/a/third/path"].join(",")
logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
final List<String> VALID_RESOURCE_PATHS = multipleWhitelistedPaths.split(",").collect { "$it/actualResource" }
HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
HttpServletRequest requestWithForwardHeader = mockRequest([forward: "another/path"])
HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "a/third/path", forward: "any/other/context/path"])
List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader]
// Act
requests.each { HttpServletRequest request ->
String generatedResourcePath = WebUtils.getResourcePath(new URI('https://nifi.apache.org/actualResource'), request, multipleWhitelistedPaths)
logger.info("Generated Resource Path: ${generatedResourcePath}")
// Assert
assert VALID_RESOURCE_PATHS.any { it == generatedResourcePath }
}
}
@Test
void testVerifyContextPathShouldAllowContextPathHeaderIfInWhitelist() throws Exception {
// Arrange
logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
String contextPath = WHITELISTED_PATH
// Act
logger.info("Testing [${contextPath}] against ${WHITELISTED_PATH}")
WebUtils.verifyContextPath(WHITELISTED_PATH, contextPath)
logger.info("Verified [${contextPath}]")
// Assert
// Would throw exception if invalid
}
@Test
void testVerifyContextPathShouldAllowContextPathHeaderIfInMultipleWhitelist() throws Exception {
// Arrange
String multipleWhitelist = [WHITELISTED_PATH, WebUtils.normalizeContextPath(WHITELISTED_PATH.reverse())].join(",")
logger.info("Whitelisted path(s): ${multipleWhitelist}")
String contextPath = WHITELISTED_PATH
// Act
logger.info("Testing [${contextPath}] against ${multipleWhitelist}")
WebUtils.verifyContextPath(multipleWhitelist, contextPath)
logger.info("Verified [${contextPath}]")
// Assert
// Would throw exception if invalid
}
@Test
void testVerifyContextPathShouldAllowContextPathHeaderIfBlank() throws Exception {
// Arrange
logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
def emptyContextPaths = ["", " ", "\t", null]
// Act
emptyContextPaths.each { String contextPath ->
logger.info("Testing [${contextPath}] against ${WHITELISTED_PATH}")
WebUtils.verifyContextPath(WHITELISTED_PATH, contextPath)
logger.info("Verified [${contextPath}]")
// Assert
// Would throw exception if invalid
}
}
@Test
void testVerifyContextPathShouldBlockContextPathHeaderIfNotAllowed() throws Exception {
// Arrange
logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
def invalidContextPaths = ["/other/path", "somesite.com", "/../trying/to/escape"]
// Act
invalidContextPaths.each { String contextPath ->
logger.info("Testing [${contextPath}] against ${WHITELISTED_PATH}")
def msg = shouldFail(UriBuilderException) {
WebUtils.verifyContextPath(WHITELISTED_PATH, contextPath)
logger.info("Verified [${contextPath}]")
}
// Assert
logger.expected(msg)
assert msg =~ " was not whitelisted "
}
}
}

View File

@ -322,4 +322,115 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
assert key == KEY_HEX assert key == KEY_HEX
assert keys == [(KEY_ID): KEY_HEX, (KEY_ID_2): KEY_HEX_2, (KEY_ID_3): KEY_HEX_3] assert keys == [(KEY_ID): KEY_HEX, (KEY_ID_2): KEY_HEX_2, (KEY_ID_3): KEY_HEX_3]
} }
@Test
void testShouldNormalizeContextPathProperty() {
// Arrange
String noLeadingSlash = "some/context/path"
Properties rawProps = new Properties(["nifi.web.proxy.context.path": noLeadingSlash])
NiFiProperties props = new StandardNiFiProperties(rawProps)
logger.info("Created a NiFiProperties instance with raw context path property [${noLeadingSlash}]")
// Act
String normalizedContextPath = props.getWhitelistedContextPaths()
logger.info("Read from NiFiProperties instance: ${normalizedContextPath}")
// Assert
assert normalizedContextPath == "/" + noLeadingSlash
}
@Test
void testShouldHandleNormalizedContextPathProperty() {
// Arrange
String leadingSlash = "/some/context/path"
Properties rawProps = new Properties(["nifi.web.proxy.context.path": leadingSlash])
NiFiProperties props = new StandardNiFiProperties(rawProps)
logger.info("Created a NiFiProperties instance with raw context path property [${leadingSlash}]")
// Act
String normalizedContextPath = props.getWhitelistedContextPaths()
logger.info("Read from NiFiProperties instance: ${normalizedContextPath}")
// Assert
assert normalizedContextPath == leadingSlash
}
@Test
void testShouldNormalizeMultipleContextPathsInProperty() {
// Arrange
String noLeadingSlash = "some/context/path"
String leadingSlash = "some/other/path"
String leadingAndTrailingSlash = "/a/third/path/"
List<String> paths = [noLeadingSlash, leadingSlash, leadingAndTrailingSlash]
String combinedPaths = paths.join(",")
Properties rawProps = new Properties(["nifi.web.proxy.context.path": combinedPaths])
NiFiProperties props = new StandardNiFiProperties(rawProps)
logger.info("Created a NiFiProperties instance with raw context path property [${noLeadingSlash}]")
// Act
String normalizedContextPath = props.getWhitelistedContextPaths()
logger.info("Read from NiFiProperties instance: ${normalizedContextPath}")
// Assert
def splitPaths = normalizedContextPath.split(",")
splitPaths.every {
assert it.startsWith("/")
assert !it.endsWith("/")
}
}
@Test
void testShouldHandleNormalizedContextPathPropertyAsList() {
// Arrange
String leadingSlash = "/some/context/path"
Properties rawProps = new Properties(["nifi.web.proxy.context.path": leadingSlash])
NiFiProperties props = new StandardNiFiProperties(rawProps)
logger.info("Created a NiFiProperties instance with raw context path property [${leadingSlash}]")
// Act
def normalizedContextPaths = props.getWhitelistedContextPathsAsList()
logger.info("Read from NiFiProperties instance: ${normalizedContextPaths}")
// Assert
assert normalizedContextPaths.size() == 1
assert normalizedContextPaths.contains(leadingSlash)
}
@Test
void testShouldNormalizeMultipleContextPathsInPropertyAsList() {
// Arrange
String noLeadingSlash = "some/context/path"
String leadingSlash = "/some/other/path"
String leadingAndTrailingSlash = "/a/third/path/"
List<String> paths = [noLeadingSlash, leadingSlash, leadingAndTrailingSlash]
String combinedPaths = paths.join(",")
Properties rawProps = new Properties(["nifi.web.proxy.context.path": combinedPaths])
NiFiProperties props = new StandardNiFiProperties(rawProps)
logger.info("Created a NiFiProperties instance with raw context path property [${noLeadingSlash}]")
// Act
def normalizedContextPaths = props.getWhitelistedContextPathsAsList()
logger.info("Read from NiFiProperties instance: ${normalizedContextPaths}")
// Assert
assert normalizedContextPaths.size() == 3
assert normalizedContextPaths.containsAll([leadingSlash, "/" + noLeadingSlash, leadingAndTrailingSlash[0..-2]])
}
@Test
void testShouldHandleNormalizingEmptyContextPathProperty() {
// Arrange
String empty = ""
Properties rawProps = new Properties(["nifi.web.proxy.context.path": empty])
NiFiProperties props = new StandardNiFiProperties(rawProps)
logger.info("Created a NiFiProperties instance with raw context path property [${empty}]")
// Act
String normalizedContextPath = props.getWhitelistedContextPaths()
logger.info("Read from NiFiProperties instance: ${normalizedContextPath}")
// Assert
assert normalizedContextPath == empty
}
} }

View File

@ -132,6 +132,7 @@
<nifi.jetty.work.dir>./work/jetty</nifi.jetty.work.dir> <nifi.jetty.work.dir>./work/jetty</nifi.jetty.work.dir>
<nifi.web.jetty.threads>200</nifi.web.jetty.threads> <nifi.web.jetty.threads>200</nifi.web.jetty.threads>
<nifi.web.max.header.size>16 KB</nifi.web.max.header.size> <nifi.web.max.header.size>16 KB</nifi.web.max.header.size>
<nifi.web.proxy.context.path />
<!-- nifi.properties: security properties --> <!-- nifi.properties: security properties -->
<nifi.security.keystore /> <nifi.security.keystore />

View File

@ -137,6 +137,7 @@ nifi.web.https.network.interface.default=${nifi.web.https.network.interface.defa
nifi.web.jetty.working.directory=${nifi.jetty.work.dir} nifi.web.jetty.working.directory=${nifi.jetty.work.dir}
nifi.web.jetty.threads=${nifi.web.jetty.threads} nifi.web.jetty.threads=${nifi.web.jetty.threads}
nifi.web.max.header.size=${nifi.web.max.header.size} nifi.web.max.header.size=${nifi.web.max.header.size}
nifi.web.proxy.context.path=${nifi.web.proxy.context.path}
# security properties # # security properties #
nifi.sensitive.props.key= nifi.sensitive.props.key=

View File

@ -0,0 +1,114 @@
/*
* 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 java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringEscapeUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.ScopedHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HostHeaderHandler extends ScopedHandler {
private static final Logger logger = LoggerFactory.getLogger(HostHeaderHandler.class);
private final String serverName;
private final int serverPort;
private final List<String> validHosts;
/**
* @param serverName the {@code serverName} to set on the request (the {@code serverPort} will not be set)
*/
public HostHeaderHandler(String serverName) {
this(serverName, 0);
}
/**
* @param serverName the {@code serverName} to set on the request
* @param serverPort the {@code serverPort} to set on the request
*/
public HostHeaderHandler(String serverName, int serverPort) {
this.serverName = Objects.requireNonNull(serverName);
this.serverPort = serverPort;
validHosts = new ArrayList<>();
validHosts.add(serverName.toLowerCase());
validHosts.add(serverName.toLowerCase() + ":" + serverPort);
// Sometimes the hostname is left empty but the port is always populated
validHosts.add("localhost");
validHosts.add("localhost:" + serverPort);
// Different from customizer -- empty is ok here
validHosts.add("");
logger.info("Created " + this.toString());
}
@Override
public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
logger.debug("HostHeaderHandler#doScope on " + request.getRequestURI());
nextScope(target, baseRequest, request, response);
}
private boolean hostHeaderIsValid(String hostHeader) {
return validHosts.contains(hostHeader.toLowerCase().trim());
}
@Override
public String toString() {
return "HostHeaderHandler for " + serverName + ":" + serverPort;
}
/**
* Returns an error message to the response and marks the request as handled if the host header is not valid.
* Otherwise passes the request on to the next scoped handler.
*
* @param target the target (not relevant here)
* @param baseRequest the original request object
* @param request the request as an HttpServletRequest
* @param response the current response
*/
@Override
public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
final String hostHeader = request.getHeader("Host");
logger.debug("Received request [" + request.getRequestURI() + "] with host header: " + hostHeader);
if (!hostHeaderIsValid(hostHeader)) {
logger.warn("Request host header [" + hostHeader + "] different from web hostname [" +
serverName + "(:" + serverPort + ")]. Overriding to [" + serverName + ":" +
serverPort + request.getRequestURI() + "]");
response.setContentType("text/html; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
out.println("<h1>System Error</h1>");
out.println("<h2>The request contained an invalid host header [" + StringEscapeUtils.escapeHtml4(hostHeader) +
"] in the request [" + StringEscapeUtils.escapeHtml4(request.getRequestURI()) +
"]. Check for request manipulation or third-party intercept. </h2>");
baseRequest.setHandled(true);
}
}
}

View File

@ -0,0 +1,80 @@
/*
* 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 java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HostHeaderSanitizationCustomizer implements HttpConfiguration.Customizer {
private static final Logger logger = LoggerFactory.getLogger(HostHeaderSanitizationCustomizer.class);
private final String serverName;
private final int serverPort;
private final List<String> validHosts;
/**
* @param serverName the {@code serverName} to set on the request (the {@code serverPort} will not be set)
*/
public HostHeaderSanitizationCustomizer(String serverName) {
this(serverName, 0);
}
/**
* @param serverName the {@code serverName} to set on the request
* @param serverPort the {@code serverPort} to set on the request
*/
public HostHeaderSanitizationCustomizer(String serverName, int serverPort) {
this.serverName = Objects.requireNonNull(serverName);
this.serverPort = serverPort;
validHosts = new ArrayList<>();
validHosts.add(serverName.toLowerCase());
validHosts.add(serverName.toLowerCase() + ":" + serverPort);
// Sometimes the hostname is left empty but the port is always populated
validHosts.add("localhost");
validHosts.add("localhost:" + serverPort);
logger.info("Created " + this.toString());
}
@Override
public void customize(Connector connector, HttpConfiguration channelConfig, Request request) {
final String hostHeader = request.getHeader("Host");
logger.debug("Received request [" + request.getRequestURI() + "] with host header: " + hostHeader);
if (!hostHeaderIsValid(hostHeader)) {
logger.warn("Request host header [" + hostHeader + "] different from web hostname [" +
serverName + "(:" + serverPort + ")]. Overriding to [" + serverName + ":" +
serverPort + request.getRequestURI() + "]");
request.setAuthority(serverName, serverPort);
}
}
private boolean hostHeaderIsValid(String hostHeader) {
return validHosts.contains(hostHeader.toLowerCase().trim());
}
@Override
public String toString() {
return "HostHeaderSanitizationCustomizer for " + serverName + ":" + serverPort;
}
}

View File

@ -18,6 +18,39 @@ package org.apache.nifi.web.server;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStreamReader;
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.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.NiFiServer; import org.apache.nifi.NiFiServer;
@ -51,6 +84,7 @@ import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterHolder;
@ -69,40 +103,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStreamReader;
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.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
/** /**
* Encapsulates the Jetty instance. * Encapsulates the Jetty instance.
*/ */
@ -153,10 +153,33 @@ public class JettyServer implements NiFiServer {
configureConnectors(server); configureConnectors(server);
// load wars from the bundle // load wars from the bundle
loadWars(bundles); Handler warHandlers = loadWars(bundles);
// Create a handler for the host header and add it to the server
String serverName = determineServerHostname();
int serverPort = determineServerPort();
HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(serverName, serverPort);
logger.info("Created HostHeaderHandler [" + hostHeaderHandler.toString() + "]");
HandlerList allHandlers = new HandlerList();
allHandlers.addHandler(hostHeaderHandler);
allHandlers.addHandler(warHandlers);
server.setHandler(allHandlers);
} }
private void loadWars(final Set<Bundle> bundles) { private int determineServerPort() {
return props.getSslPort() != null ? props.getSslPort() : props.getPort();
}
private String determineServerHostname() {
if (props.getSslPort() != null) {
return props.getProperty(NiFiProperties.WEB_HTTPS_HOST, "localhost");
} else {
return props.getProperty(NiFiProperties.WEB_HTTP_HOST, "localhost");
}
}
private Handler loadWars(final Set<Bundle> bundles) {
// load WARs // load WARs
final Map<File, Bundle> warToBundleLookup = findWars(bundles); final Map<File, Bundle> warToBundleLookup = findWars(bundles);
@ -316,17 +339,19 @@ public class JettyServer implements NiFiServer {
handlers.addHandler(documentationHandlers); handlers.addHandler(documentationHandlers);
// load the web error app // load the web error app
handlers.addHandler(loadWar(webErrorWar, "/", frameworkClassLoader)); final WebAppContext webErrorContext = loadWar(webErrorWar, "/", frameworkClassLoader);
webErrorContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths());
handlers.addHandler(webErrorContext);
// deploy the web apps // deploy the web apps
server.setHandler(gzip(handlers)); return gzip(handlers);
} }
/** /**
* Returns whether or not the specified ui extensions already contains an extension of the specified type. * Returns whether or not the specified ui extensions already contains an extension of the specified type.
* *
* @param componentUiExtensionsForType ui extensions for the type * @param componentUiExtensionsForType ui extensions for the type
* @param extensionType type of ui extension * @param extensionType type of ui extension
* @return whether or not the specified ui extensions already contains an extension of the specified type * @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) { private boolean containsUiExtensionType(final List<UiExtension> componentUiExtensionsForType, final UiExtensionType extensionType) {
@ -409,7 +434,7 @@ public class JettyServer implements NiFiServer {
* Identifies all known UI extensions and stores them in the specified map. * Identifies all known UI extensions and stores them in the specified map.
* *
* @param uiExtensions extensions * @param uiExtensions extensions
* @param warFile war * @param warFile war
*/ */
private void identifyUiExtensionsForComponents(final Map<UiExtensionType, List<String>> uiExtensions, final File warFile) { private void identifyUiExtensionsForComponents(final Map<UiExtensionType, List<String>> uiExtensions, final File warFile) {
try (final JarFile jarFile = new JarFile(warFile)) { try (final JarFile jarFile = new JarFile(warFile)) {
@ -445,7 +470,7 @@ public class JettyServer implements NiFiServer {
webappContext.setDisplayName(contextPath); webappContext.setDisplayName(contextPath);
// instruction jetty to examine these jars for tlds, web-fragments, etc // instruction jetty to examine these jars for tlds, web-fragments, etc
webappContext.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$" ); webappContext.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$");
// remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib // 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())); List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses()));
@ -522,7 +547,7 @@ public class JettyServer implements NiFiServer {
/** /**
* Returns a File object for the directory containing NIFI documentation. * Returns a File object for the directory containing NIFI documentation.
* * <p>
* Formerly, if the docsDirectory did not exist NIFI would fail to start * Formerly, if the docsDirectory did not exist NIFI would fail to start
* with an IllegalStateException and a rather unhelpful log message. * with an IllegalStateException and a rather unhelpful log message.
* NIFI-2184 updates the process such that if the docsDirectory does not * NIFI-2184 updates the process such that if the docsDirectory does not
@ -582,6 +607,8 @@ public class JettyServer implements NiFiServer {
httpConfiguration.setRequestHeaderSize(headerSize); httpConfiguration.setRequestHeaderSize(headerSize);
httpConfiguration.setResponseHeaderSize(headerSize); httpConfiguration.setResponseHeaderSize(headerSize);
addHostHeaderSanitizationCustomizer(httpConfiguration);
if (props.getPort() != null) { if (props.getPort() != null) {
final Integer port = props.getPort(); final Integer port = props.getPort();
if (port < 0 || (int) Math.pow(2, 16) <= port) { if (port < 0 || (int) Math.pow(2, 16) <= port) {
@ -683,6 +710,7 @@ public class JettyServer implements NiFiServer {
httpsConfiguration.setSecureScheme("https"); httpsConfiguration.setSecureScheme("https");
httpsConfiguration.setSecurePort(props.getSslPort()); httpsConfiguration.setSecurePort(props.getSslPort());
httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
addHostHeaderSanitizationCustomizer(httpsConfiguration);
// build the connector // build the connector
return new ServerConnector(server, return new ServerConnector(server,
@ -690,6 +718,18 @@ public class JettyServer implements NiFiServer {
new HttpConnectionFactory(httpsConfiguration)); new HttpConnectionFactory(httpsConfiguration));
} }
private void addHostHeaderSanitizationCustomizer(HttpConfiguration httpConfiguration) {
// Add the HostHeaderCustomizer to the configuration
HttpConfiguration.Customizer hostHeaderCustomizer;
if (props.getSslPort() != null) {
hostHeaderCustomizer = new HostHeaderSanitizationCustomizer(props.getProperty(NiFiProperties.WEB_HTTPS_HOST), props.getSslPort());
} else {
hostHeaderCustomizer = new HostHeaderSanitizationCustomizer(props.getProperty(NiFiProperties.WEB_HTTP_HOST), props.getPort());
}
httpConfiguration.addCustomizer(hostHeaderCustomizer);
logger.info("Added HostHeaderSanitizationCustomizer to HttpConfiguration: " + hostHeaderCustomizer);
}
private SslContextFactory createSslContextFactory() { private SslContextFactory createSslContextFactory() {
final SslContextFactory contextFactory = new SslContextFactory(); final SslContextFactory contextFactory = new SslContextFactory();
configureSslContextFactory(contextFactory, props); configureSslContextFactory(contextFactory, props);

View File

@ -16,8 +16,40 @@
*/ */
package org.apache.nifi.web.api; package org.apache.nifi.web.api;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_NAME;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_VALUE;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.authorization.AuthorizableLookup; import org.apache.nifi.authorization.AuthorizableLookup;
import org.apache.nifi.authorization.AuthorizeAccess; import org.apache.nifi.authorization.AuthorizeAccess;
@ -54,42 +86,10 @@ import org.apache.nifi.web.api.entity.Entity;
import org.apache.nifi.web.api.entity.TransactionResultEntity; import org.apache.nifi.web.api.entity.TransactionResultEntity;
import org.apache.nifi.web.security.ProxiedEntitiesUtils; import org.apache.nifi.web.security.ProxiedEntitiesUtils;
import org.apache.nifi.web.security.util.CacheKey; import org.apache.nifi.web.security.util.CacheKey;
import org.apache.nifi.web.util.WebUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_NAME;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_VALUE;
/** /**
* Base class for controllers. * Base class for controllers.
*/ */
@ -115,10 +115,10 @@ public abstract class ApplicationResource {
public static final String NODEWISE = "false"; public static final String NODEWISE = "false";
@Context @Context
private HttpServletRequest httpServletRequest; protected HttpServletRequest httpServletRequest;
@Context @Context
private UriInfo uriInfo; protected UriInfo uriInfo;
protected NiFiProperties properties; protected NiFiProperties properties;
private RequestReplicator requestReplicator; private RequestReplicator requestReplicator;
@ -141,26 +141,14 @@ public abstract class ApplicationResource {
try { try {
// check for proxy settings // check for proxy settings
final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER); final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER);
final String host = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER); final String host = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
final String port = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER); final String port = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
String baseContextPath = getFirstHeaderValue(PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER);
// if necessary, prepend the context path // Catch header poisoning
String resourcePath = uri.getPath(); String whitelistedContextPaths = properties.getWhitelistedContextPaths();
if (baseContextPath != null) { String resourcePath = WebUtils.getResourcePath(uri, httpServletRequest, whitelistedContextPaths);
// normalize context path
if (!baseContextPath.startsWith("/")) {
baseContextPath = "/" + baseContextPath;
}
if (baseContextPath.endsWith("/")) {
baseContextPath = StringUtils.substringBeforeLast(baseContextPath, "/");
}
// determine the complete resource path
resourcePath = baseContextPath + resourcePath;
}
// determine the port uri // determine the port uri
int uriPort = uri.getPort(); int uriPort = uri.getPort();
@ -462,13 +450,13 @@ public abstract class ApplicationResource {
/** /**
* Authorizes the specified process group. * Authorizes the specified process group.
* *
* @param processGroupAuthorizable process group * @param processGroupAuthorizable process group
* @param authorizer authorizer * @param authorizer authorizer
* @param lookup lookup * @param lookup lookup
* @param action action * @param action action
* @param authorizeReferencedServices whether to authorize referenced services * @param authorizeReferencedServices whether to authorize referenced services
* @param authorizeTemplates whether to authorize templates * @param authorizeTemplates whether to authorize templates
* @param authorizeControllerServices whether to authorize controller services * @param authorizeControllerServices whether to authorize controller services
*/ */
protected void authorizeProcessGroup(final ProcessGroupAuthorizable processGroupAuthorizable, final Authorizer authorizer, final AuthorizableLookup lookup, final RequestAction action, protected void authorizeProcessGroup(final ProcessGroupAuthorizable processGroupAuthorizable, final Authorizer authorizer, final AuthorizableLookup lookup, final RequestAction action,
final boolean authorizeReferencedServices, final boolean authorizeTemplates, final boolean authorizeReferencedServices, final boolean authorizeTemplates,
@ -857,8 +845,8 @@ public abstract class ApplicationResource {
/** /**
* Replicates the request to the given node * Replicates the request to the given node
* *
* @param method the HTTP method * @param method the HTTP method
* @param entity the Entity to replicate * @param entity the Entity to replicate
* @param nodeUuid the UUID of the node to replicate the request to * @param nodeUuid the UUID of the node to replicate the request to
* @return the response from the node * @return the response from the node
* @throws UnknownNodeException if the nodeUuid given does not map to any node in the cluster * @throws UnknownNodeException if the nodeUuid given does not map to any node in the cluster
@ -963,7 +951,7 @@ public abstract class ApplicationResource {
* @throws InterruptedException if interrupted while replicating the request * @throws InterruptedException if interrupted while replicating the request
*/ */
protected NodeResponse replicateNodeResponse(final String method) throws InterruptedException { protected NodeResponse replicateNodeResponse(final String method) throws InterruptedException {
return replicateNodeResponse(method, getRequestParameters(), (Map<String, String>) null); return replicateNodeResponse(method, getRequestParameters(), null);
} }
/** /**
@ -1084,8 +1072,8 @@ public abstract class ApplicationResource {
return properties; return properties;
} }
public static enum ReplicationTarget { public enum ReplicationTarget {
CLUSTER_NODES, CLUSTER_COORDINATOR; CLUSTER_NODES, CLUSTER_COORDINATOR
} }
// ----------------- // -----------------

View File

@ -0,0 +1,181 @@
/*
* 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.api
import org.apache.nifi.properties.StandardNiFiProperties
import org.apache.nifi.util.NiFiProperties
import org.glassfish.jersey.uri.internal.JerseyUriBuilder
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.UriBuilderException
import javax.ws.rs.core.UriInfo
@RunWith(JUnit4.class)
class ApplicationResourceTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(ApplicationResourceTest.class)
static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme"
static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort"
static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath"
static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto"
static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port"
static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context"
static final String PROXY_CONTEXT_PATH_PROP = NiFiProperties.WEB_PROXY_CONTEXT_PATH
static final String WHITELISTED_PATH = "/some/context/path"
@BeforeClass
static void setUpOnce() throws Exception {
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
}
class MockApplicationResource extends ApplicationResource {
void setHttpServletRequest(HttpServletRequest request) {
super.httpServletRequest = request
}
void setUriInfo(UriInfo uriInfo) {
super.uriInfo = uriInfo
}
}
private ApplicationResource buildApplicationResource() {
ApplicationResource resource = new MockApplicationResource()
HttpServletRequest mockRequest = [getHeader: { String k ->
logger.mock("Request.getHeader($k)")
if ([FORWARDED_CONTEXT_HTTP_HEADER, PROXY_CONTEXT_PATH_HTTP_HEADER].contains(k)) {
WHITELISTED_PATH
} else if ([FORWARDED_PORT_HTTP_HEADER, PROXY_PORT_HTTP_HEADER].contains(k)) {
"8081"
} else if ([FORWARDED_PROTO_HTTP_HEADER, PROXY_SCHEME_HTTP_HEADER].contains(k)) {
"https"
} else {
"nifi.apache.org"
}
}, getContextPath: { ->
logger.mock("Request.getContextPath()")
""
}] as HttpServletRequest
UriInfo mockUriInfo = [getBaseUriBuilder: { ->
logger.mock("Returning mock UriBuilder")
new JerseyUriBuilder().uri(new URI('https://nifi.apache.org/'))
}] as UriInfo
resource.setHttpServletRequest(mockRequest)
resource.setUriInfo(mockUriInfo)
resource.properties = new StandardNiFiProperties()
resource
}
@Test
void testGenerateUriShouldBlockProxyContextPathHeaderIfNotInWhitelist() throws Exception {
// Arrange
ApplicationResource resource = buildApplicationResource()
logger.info("Whitelisted path(s): ")
// Act
def msg = shouldFail(UriBuilderException) {
String generatedUri = resource.generateResourceUri('actualResource')
logger.unexpected("Generated URI: ${generatedUri}")
}
// Assert
logger.expected(msg)
assert msg =~ "The provided context path \\[.*\\] was not whitelisted \\[\\]"
}
@Test
void testGenerateUriShouldAllowProxyContextPathHeaderIfInWhitelist() throws Exception {
// Arrange
ApplicationResource resource = buildApplicationResource()
logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): WHITELISTED_PATH] as Properties)
resource.properties = niFiProperties
// Act
String generatedUri = resource.generateResourceUri('actualResource')
logger.info("Generated URI: ${generatedUri}")
// Assert
assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
}
@Test
void testGenerateUriShouldAllowProxyContextPathHeaderIfElementInMultipleWhitelist() throws Exception {
// Arrange
ApplicationResource resource = buildApplicationResource()
String multipleWhitelistedPaths = [WHITELISTED_PATH, "another/path", "a/third/path"].join(",")
logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleWhitelistedPaths] as Properties)
resource.properties = niFiProperties
// Act
String generatedUri = resource.generateResourceUri('actualResource')
logger.info("Generated URI: ${generatedUri}")
// Assert
assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
}
@Test
void testGenerateUriShouldBlockForwardedContextHeaderIfNotInWhitelist() throws Exception {
// Arrange
// Act
// Assert
}
@Test
void testGenerateUriShouldAllowForwardedContextHeaderIfInWhitelist() throws Exception {
// Arrange
// Act
// Assert
}
@Test
void testGenerateUriShouldAllowForwardedContextHeaderIfElementInMultipleWhitelist() throws Exception {
// Arrange
// Act
// Assert
}
}

View File

@ -30,6 +30,7 @@
<logger name="org.apache.nifi" level="INFO"/> <logger name="org.apache.nifi" level="INFO"/>
<logger name="org.apache.nifi.web.api" level="DEBUG"/>
<root level="INFO"> <root level="INFO">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
</root> </root>

View File

@ -31,5 +31,10 @@
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-utils</artifactId>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -23,18 +23,35 @@ import javax.servlet.FilterConfig;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.UriBuilderException;
import org.apache.nifi.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Filter for forward all requests to index.jsp. * Filter for forward all requests to index.jsp.
*/ */
public class CatchAllFilter implements Filter { public class CatchAllFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(CatchAllFilter.class);
private static String whitelistedContextPaths = "";
@Override @Override
public void init(FilterConfig filterConfig) throws ServletException { public void init(FilterConfig filterConfig) throws ServletException {
String providedWhitelist = filterConfig.getServletContext().getInitParameter("whitelistedContextPaths");
logger.debug("CatchAllFilter received provided whitelisted context paths from NiFi properties: " + providedWhitelist);
if (providedWhitelist != null) {
whitelistedContextPaths = providedWhitelist;
}
} }
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// Capture the provided context path headers and sanitize them before using in the response
String contextPath = getSanitizedContextPath(request);
request.setAttribute("contextPath", contextPath);
// for all requests to index.jsp // for all requests to index.jsp
request.getRequestDispatcher("/index.jsp").forward(request, response); request.getRequestDispatcher("/index.jsp").forward(request, response);
} }
@ -42,4 +59,23 @@ public class CatchAllFilter implements Filter {
@Override @Override
public void destroy() { public void destroy() {
} }
/**
* Returns a "safe" context path value from the request headers to use in a proxy environment.
* This is used on the JSP to build the resource paths for the external resources (CSS, JS, etc.).
* If no headers are present specifying this value, it is an empty string.
*
* @param request the HTTP request
* @return the context path safe to be printed to the page
*/
private String getSanitizedContextPath(ServletRequest request) {
String contextPath = WebUtils.normalizeContextPath(WebUtils.determineContextPath((HttpServletRequest) request));
try {
WebUtils.verifyContextPath(whitelistedContextPaths, contextPath);
return contextPath;
} catch (UriBuilderException e) {
logger.error("Error determining context path on index.jsp: " + e.getMessage());
return "";
}
}
} }

View File

@ -17,36 +17,29 @@
<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %> <%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<% <%
String contextPath = request.getHeader("X-ProxyContextPath"); // Sanitize the contextPath to ensure it is on this server
if (contextPath == null) { // rather than getting it from the header directly
contextPath = request.getHeader("X-Forwarded-Context"); String contextPath = request.getAttribute("contextPath").toString();
} %>
if (contextPath == null) { <head>
contextPath = ""; <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
} <link rel="shortcut icon" href="<%= contextPath %>/nifi/images/nifi16.ico"/>
if (contextPath.endsWith("/")) { <title>NiFi</title>
contextPath = contextPath.substring(0, contextPath.length() - 1); <link rel="stylesheet" href="<%= contextPath %>/nifi/assets/reset.css/reset.css" type="text/css"/>
} <link rel="stylesheet" href="<%= contextPath %>/nifi/css/common-ui.css" type="text/css"/>
%> <link rel="stylesheet" href="<%= contextPath %>/nifi/fonts/flowfont/flowfont.css" type="text/css"/>
<head> <link rel="stylesheet" href="<%= contextPath %>/nifi/assets/font-awesome/css/font-awesome.min.css" type="text/css"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" href="<%= contextPath %>/nifi/css/message-pane.css" type="text/css"/>
<link rel="shortcut icon" href="<%= contextPath %>/nifi/images/nifi16.ico"/> <link rel="stylesheet" href="<%= contextPath %>/nifi/css/message-page.css" type="text/css"/>
<title>NiFi</title> </head>
<link rel="stylesheet" href="<%= contextPath %>/nifi/assets/reset.css/reset.css" type="text/css" />
<link rel="stylesheet" href="<%= contextPath %>/nifi/css/common-ui.css" type="text/css" />
<link rel="stylesheet" href="<%= contextPath %>/nifi/fonts/flowfont/flowfont.css" type="text/css" />
<link rel="stylesheet" href="<%= contextPath %>/nifi/assets/font-awesome/css/font-awesome.min.css" type="text/css" />
<link rel="stylesheet" href="<%= contextPath %>/nifi/css/message-pane.css" type="text/css" />
<link rel="stylesheet" href="<%= contextPath %>/nifi/css/message-page.css" type="text/css" />
</head>
<body class="message-pane"> <body class="message-pane">
<div class="message-pane-message-box"> <div class="message-pane-message-box">
<p class="message-pane-title"> <p class="message-pane-title">
Did you mean: <a href="<%= contextPath %>/nifi/">/nifi</a> Did you mean: <a href="<%= contextPath %>/nifi/">/nifi</a>
</p> </p>
<p class="message-pane-content">You may have mistyped...</p> <p class="message-pane-content">You may have mistyped...</p>
</div> </div>
</body> </body>
</html> </html>

View File

@ -16,6 +16,20 @@
*/ */
package org.apache.nifi.processors.standard; package org.apache.nifi.processors.standard;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import javax.servlet.Servlet;
import javax.ws.rs.Path;
import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.CapabilityDescription;
@ -49,21 +63,6 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool;
import javax.servlet.Servlet;
import javax.ws.rs.Path;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
@InputRequirement(Requirement.INPUT_FORBIDDEN) @InputRequirement(Requirement.INPUT_FORBIDDEN)
@Tags({"ingest", "http", "https", "rest", "listen"}) @Tags({"ingest", "http", "https", "rest", "listen"})
@CapabilityDescription("Starts an HTTP Server and listens on a given base path to transform incoming requests into FlowFiles. " @CapabilityDescription("Starts an HTTP Server and listens on a given base path to transform incoming requests into FlowFiles. "
@ -206,7 +205,7 @@ public class ListenHTTP extends AbstractSessionFactoryProcessor {
final StreamThrottler streamThrottler = (maxBytesPerSecond == null) ? null : new LeakyBucketStreamThrottler(maxBytesPerSecond.intValue()); final StreamThrottler streamThrottler = (maxBytesPerSecond == null) ? null : new LeakyBucketStreamThrottler(maxBytesPerSecond.intValue());
throttlerRef.set(streamThrottler); throttlerRef.set(streamThrottler);
final boolean needClientAuth = sslContextService == null ? false : sslContextService.getTrustStoreFile() != null; final boolean needClientAuth = sslContextService != null && sslContextService.getTrustStoreFile() != null;
final SslContextFactory contextFactory = new SslContextFactory(); final SslContextFactory contextFactory = new SslContextFactory();
contextFactory.setNeedClientAuth(needClientAuth); contextFactory.setNeedClientAuth(needClientAuth);

View File

@ -16,6 +16,12 @@
*/ */
package org.apache.nifi.websocket.jetty; package org.apache.nifi.websocket.jetty;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnDisabled; import org.apache.nifi.annotation.lifecycle.OnDisabled;
@ -49,13 +55,6 @@ import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Tags({"WebSocket", "Jetty", "server"}) @Tags({"WebSocket", "Jetty", "server"})
@CapabilityDescription("Implementation of WebSocketServerService." + @CapabilityDescription("Implementation of WebSocketServerService." +
" This service uses Jetty WebSocket server module to provide" + " This service uses Jetty WebSocket server module to provide" +