mirror of https://github.com/apache/nifi.git
NIFI-4501
- Changed request header handling logic. - Removed unnecessary Maven dependency. - This closes #2279
This commit is contained in:
parent
c89d793364
commit
5d643edfab
|
@ -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";
|
||||||
|
@ -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 ->
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -68,7 +73,6 @@ public final class WebUtils {
|
||||||
*
|
*
|
||||||
* @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) {
|
||||||
|
@ -83,7 +87,6 @@ public final class WebUtils {
|
||||||
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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=
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,10 +339,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------
|
// -----------------
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,9 @@
|
||||||
<!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) {
|
|
||||||
contextPath = "";
|
|
||||||
}
|
|
||||||
if (contextPath.endsWith("/")) {
|
|
||||||
contextPath = contextPath.substring(0, contextPath.length() - 1);
|
|
||||||
}
|
|
||||||
%>
|
%>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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" +
|
||||||
|
|
Loading…
Reference in New Issue