Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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_THREADS = "nifi.web.jetty.threads";
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
public static final String UI_BANNER_TEXT = "nifi.ui.banner.text";
Expand Down Expand Up @@ -256,8 +257,8 @@ public abstract class NiFiProperties {
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_AUTH_TYPE = "default";
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_HOST_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_FLOW_CONFIGURATION_ARCHIVE_ENABLED = "true";
public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_TIME = "30 days";
Expand Down Expand Up @@ -1084,7 +1085,7 @@ public Map<String, Path> getProvenanceRepositoryPaths() {
* 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
* to get too large because it will hold open up to this many FileOutputStreams.
*
* <p>
* Default is {@link #DEFAULT_MAX_FLOWFILES_PER_CLAIM}
*
* @return the maximum number of flow files per claim
Expand All @@ -1100,7 +1101,7 @@ public int getMaxFlowFilesPerClaim() {
/**
* 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.
*
* <p>
* Default is {@link #DEFAULT_MAX_APPENDABLE_CLAIM_SIZE}
*
* @return the maximum appendable claim size
Expand Down Expand Up @@ -1285,6 +1286,42 @@ public Map<String, String> getProvenanceRepoEncryptionKeys() {
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() {
// Filter all the property keys that define a key
return getPropertyKeys().stream().filter(k ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,41 @@
*/
package org.apache.nifi.web.util;

import org.apache.nifi.security.util.CertificateUtils;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import org.slf4j.Logger;
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.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.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Common utilities related to web development.
*
*/
public final class WebUtils {

private static Logger logger = LoggerFactory.getLogger(WebUtils.class);

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() {
}

Expand All @@ -54,7 +60,6 @@ private WebUtils() {
* automatically configured for JSON serialization/deserialization.
*
* @param config client configuration
*
* @return a Client instance
*/
public static Client createClient(final ClientConfig config) {
Expand All @@ -67,8 +72,7 @@ public static Client createClient(final ClientConfig config) {
* will be automatically configured for JSON serialization/deserialization.
*
* @param config client configuration
* @param ctx security context
*
* @param ctx security context
* @return a Client instance
*/
public static Client createClient(final ClientConfig config, final SSLContext ctx) {
Expand All @@ -81,9 +85,8 @@ public static Client createClient(final ClientConfig config, final SSLContext ct
* will be automatically configured for JSON serialization/deserialization.
*
* @param config client configuration
* @param ctx security context, which may be null for non-secure client
* creation
*
* @param ctx security context, which may be null for non-secure client
* creation
* @return a Client instance
*/
private static Client createClientHelper(final ClientConfig config, final SSLContext ctx) {
Expand Down Expand Up @@ -128,4 +131,119 @@ public boolean verify(final String hostname, final SSLSession ssls) {

}

/**
* 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;
}

}
Loading