NIFI-9481 Excluded Data Transfer REST methods from DoSFilter

- Added DataTransferDoSFilter with request URI evaluation
- Added RequestFilterProvider and implementations to abstract Jetty Filter configuration

Signed-off-by: Joe Gresock <jgresock@gmail.com>

This closes #5670.
This commit is contained in:
exceptionfactory 2022-01-18 17:19:06 -06:00 committed by Joe Gresock
parent 70aa719cbf
commit fc27b3138b
No known key found for this signature in database
GPG Key ID: 37F5B9B6E258C8B7
10 changed files with 582 additions and 585 deletions

View File

@ -54,13 +54,10 @@ import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.ContentAccess;
import org.apache.nifi.web.NiFiWebConfigurationContext;
import org.apache.nifi.web.UiExtensionType;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.apache.nifi.web.security.requests.ContentLengthFilter;
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
import org.apache.nifi.web.server.filter.FilterParameter;
import org.apache.nifi.web.server.filter.RequestFilterProvider;
import org.apache.nifi.web.server.filter.RestApiRequestFilterProvider;
import org.apache.nifi.web.server.filter.StandardRequestFilterProvider;
import org.apache.nifi.web.server.log.RequestLogProvider;
import org.apache.nifi.web.server.log.StandardRequestLogProvider;
import org.apache.nifi.web.server.util.TrustStoreScanner;
@ -83,7 +80,6 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.DoSFilter;
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@ -99,7 +95,6 @@ import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import java.io.BufferedReader;
import java.io.BufferedWriter;
@ -147,9 +142,9 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
private static final String CONTEXT_PATH_NIFI_API = "/nifi-api";
private static final String CONTEXT_PATH_NIFI_CONTENT_VIEWER = "/nifi-content-viewer";
private static final String CONTEXT_PATH_NIFI_DOCS = "/nifi-docs";
private static final String RELATIVE_PATH_ACCESS_TOKEN = "/access/token";
private static final int DOS_FILTER_REJECT_REQUEST = -1;
private static final RequestFilterProvider REQUEST_FILTER_PROVIDER = new StandardRequestFilterProvider();
private static final RequestFilterProvider REST_API_REQUEST_FILTER_PROVIDER = new RestApiRequestFilterProvider();
private static final FileFilter WAR_FILTER = pathname -> {
final String nameToTest = pathname.getName().toLowerCase();
@ -214,15 +209,11 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
if (props.isHTTPSConfigured()) {
// Create a handler for the host header and add it to the server
final HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
logger.info("Created HostHeaderHandler [{}}]", hostHeaderHandler);
// Add this before the WAR handlers
allHandlers.addHandler(hostHeaderHandler);
} else {
logger.info("Running in HTTP mode; host headers not restricted");
}
final ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
contextHandlers.addHandler(warHandlers);
allHandlers.addHandler(contextHandlers);
@ -239,14 +230,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
server.setRequestLog(requestLog);
}
/**
* Instantiates this object but does not perform any configuration. Used for unit testing.
*/
JettyServer(Server server, NiFiProperties properties) {
this.server = server;
this.props = properties;
}
private Handler loadInitialWars(final Set<Bundle> bundles) {
// load WARs
@ -633,25 +616,15 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
// configure the max form size (3x the default)
webappContext.setMaxFormContentSize(600000);
// add HTTP security headers to all responses
// TODO: Allow more granular path configuration (e.g. /nifi-api/site-to-site/ vs. /nifi-api/process-groups)
ArrayList<Class<? extends Filter>> filters =
new ArrayList<>(Arrays.asList(
XFrameOptionsFilter.class,
ContentSecurityPolicyFilter.class,
XSSProtectionFilter.class,
XContentTypeOptionsFilter.class));
final List<FilterHolder> requestFilters = CONTEXT_PATH_NIFI_API.equals(contextPath)
? REST_API_REQUEST_FILTER_PROVIDER.getFilters(props)
: REQUEST_FILTER_PROVIDER.getFilters(props);
if (props.isHTTPSConfigured()) {
filters.add(StrictTransportSecurityFilter.class);
filters.add(RequestAuthenticationFilter.class);
}
filters.forEach((filter) -> addFilters(filter, webappContext));
addDenialOfServiceFilters(webappContext, props);
if (CONTEXT_PATH_NIFI_API.equals(contextPath)) {
addAccessTokenRequestFilter(webappContext, props);
}
requestFilters.forEach(filter -> {
final String pathSpecification = filter.getInitParameter(FilterParameter.PATH_SPECIFICATION.name());
final String filterPathSpecification = pathSpecification == null ? CONTEXT_PATH_ALL : pathSpecification;
webappContext.addFilter(filter, filterPathSpecification, EnumSet.allOf(DispatcherType.class));
});
try {
// configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ...
@ -660,16 +633,10 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
startUpFailure(ioe);
}
logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
logger.info("Loading WAR [{}] Context Path [{}]", warFile.getAbsolutePath(), contextPath);
return webappContext;
}
private void addFilters(Class<? extends Filter> clazz, WebAppContext webappContext) {
FilterHolder holder = new FilterHolder(clazz);
holder.setName(clazz.getSimpleName());
webappContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
}
private void addDocsServlets(WebAppContext docsContext) {
try {
// Load the nifi/docs directory
@ -712,108 +679,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
}
}
/**
* Adds configurable filters relating to preventing denial of service attacks to the given context.
* Currently, this implementation adds
* {@link org.eclipse.jetty.servlets.DoSFilter} and {@link ContentLengthFilter} filters.
*
* @param webAppContext context to which filters will be added
* @param props the {@link NiFiProperties}
*/
private static void addDenialOfServiceFilters(final WebAppContext webAppContext, final NiFiProperties props) {
addWebRequestLimitingFilter(webAppContext, props.getMaxWebRequestsPerSecond(), getWebRequestTimeoutMs(props), props.getWebRequestIpWhitelist());
// Only add the ContentLengthFilter if the property is explicitly set (empty by default)
final int maxRequestSize = determineMaxRequestSize(props);
if (maxRequestSize > 0) {
addContentLengthFilter(webAppContext, maxRequestSize);
} else {
logger.debug("Not adding content-length filter because {} is not set in nifi.properties", NiFiProperties.WEB_MAX_CONTENT_SIZE);
}
}
private static long getWebRequestTimeoutMs(final NiFiProperties props) {
final long defaultRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_WEB_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS));
long configuredRequestTimeout = 0L;
try {
configuredRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(props.getWebRequestTimeout(), TimeUnit.MILLISECONDS));
} catch (final NumberFormatException e) {
logger.warn("Exception parsing property [{}]; using default value: [{}]", NiFiProperties.WEB_REQUEST_TIMEOUT, defaultRequestTimeout);
}
return configuredRequestTimeout > 0 ? configuredRequestTimeout : defaultRequestTimeout;
}
/**
* Adds the {@link org.eclipse.jetty.servlets.DoSFilter} to the specified context and path. Limits incoming web requests to {@code maxWebRequestsPerSecond} per second.
* In order to allow clients to make more requests than the maximum rate, clients can be added to the {@code ipWhitelist}.
* The {@code requestTimeoutInMilliseconds} value limits requests to the given request timeout amount, and will close connections that run longer than this time.
*
* @param webAppContext Web Application Context where Filter will be added
* @param maxRequestsPerSec Maximum number of allowed requests per second
* @param maxRequestMs Maximum amount of time in milliseconds before a connection will be automatically closed
* @param allowed Comma-separated string of IP addresses that should not be rate limited. Does not apply to request timeout
*/
private static void addWebRequestLimitingFilter(final WebAppContext webAppContext, final int maxRequestsPerSec, final long maxRequestMs, final String allowed) {
final FilterHolder holder = new FilterHolder(DoSFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
put("maxRequestMs", Long.toString(maxRequestMs));
put("ipWhitelist", allowed);
}});
holder.setName(DoSFilter.class.getSimpleName());
webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", CONTEXT_PATH_ALL, maxRequestsPerSec, maxRequestMs, allowed);
}
private static void addAccessTokenRequestFilter(final WebAppContext webAppContext, final NiFiProperties properties) {
final int maxRequestsPerSec = properties.getMaxWebAccessTokenRequestsPerSecond();
final long maxRequestMs = getWebRequestTimeoutMs(properties);
final String webRequestAllowed = properties.getWebRequestIpWhitelist();
final FilterHolder holder = new FilterHolder(DoSFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
put("maxRequestMs", Long.toString(maxRequestMs));
put("ipWhitelist", webRequestAllowed);
put("maxWaitMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
put("delayMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
}});
holder.setName("AccessTokenRequest-DoSFilter");
webAppContext.addFilter(holder, RELATIVE_PATH_ACCESS_TOKEN, EnumSet.allOf(DispatcherType.class));
logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", RELATIVE_PATH_ACCESS_TOKEN, maxRequestsPerSec, maxRequestMs, webRequestAllowed);
}
private static int determineMaxRequestSize(NiFiProperties props) {
try {
final String webMaxContentSize = props.getWebMaxContentSize();
logger.debug("Read {} as {}", NiFiProperties.WEB_MAX_CONTENT_SIZE, webMaxContentSize);
if (StringUtils.isNotBlank(webMaxContentSize)) {
int configuredMaxRequestSize = DataUnit.parseDataSize(webMaxContentSize, DataUnit.B).intValue();
logger.debug("Parsed max content length as {} bytes", configuredMaxRequestSize);
return configuredMaxRequestSize;
} else {
logger.debug("{} read from nifi.properties is empty", NiFiProperties.WEB_MAX_CONTENT_SIZE);
}
} catch (final IllegalArgumentException e) {
logger.warn("Exception parsing property {}; disabling content length filter", NiFiProperties.WEB_MAX_CONTENT_SIZE);
logger.debug("Error during parsing: ", e);
}
return -1;
}
private static void addContentLengthFilter(final WebAppContext webAppContext, int maxContentLength) {
final FilterHolder holder = new FilterHolder(ContentLengthFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
put("maxContentLength", String.valueOf(maxContentLength));
}});
holder.setName(ContentLengthFilter.class.getSimpleName());
logger.debug("Adding ContentLengthFilter to Path [{}] with Maximum Content Length [{}B]", CONTEXT_PATH_ALL, maxContentLength);
webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
}
/**
* Returns a File object for the directory containing NIFI documentation.
* <p>

View File

@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.server.filter;
import org.eclipse.jetty.servlets.DoSFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Denial-of-Service Filter extended to exclude Data Transfer operations
*/
public class DataTransferExcludedDoSFilter extends DoSFilter {
protected static final String DATA_TRANSFER_URI_ATTRIBUTE = "nifi-api-data-transfer-uri";
private static final String DATA_TRANSFER_PATH = "/nifi-api/data-transfer";
/**
* Handle Filter Chain and override service filter for Data Transfer requests
*
* @param filterChain Filter Chain
* @param request HTTP Servlet Request to be evaluated
* @param response HTTP Servlet Response
* @throws ServletException Thrown on FilterChain.doFilter() failures
* @throws IOException Thrown on FilterChain.doFilter() failures
*/
@Override
protected void doFilterChain(final FilterChain filterChain, final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
final String requestUri = request.getRequestURI();
if (requestUri.startsWith(DATA_TRANSFER_PATH)) {
request.setAttribute(DATA_TRANSFER_URI_ATTRIBUTE, requestUri);
filterChain.doFilter(request, response);
} else {
super.doFilterChain(filterChain, request, response);
}
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.filter;
/**
* Filter Parameter enumeration
*/
public enum FilterParameter {
PATH_SPECIFICATION
}

View File

@ -0,0 +1,35 @@
/*
* 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.filter;
import org.apache.nifi.util.NiFiProperties;
import org.eclipse.jetty.servlet.FilterHolder;
import java.util.List;
/**
* Request Filter Provider for abstracting configuration of HTTP Request Filters
*/
public interface RequestFilterProvider {
/**
* Get Filters using provided NiFi Properties
*
* @param properties NiFi Properties required
* @return List of Filter Holder
*/
List<FilterHolder> getFilters(NiFiProperties properties);
}

View File

@ -0,0 +1,62 @@
/*
* 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.filter;
import org.apache.nifi.util.NiFiProperties;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlets.DoSFilter;
import java.util.List;
/**
* Request Filter Provider for REST API Web Application
*/
public class RestApiRequestFilterProvider extends StandardRequestFilterProvider {
public static final String RELATIVE_PATH_ACCESS_TOKEN = "/access/token";
private static final int DOS_FILTER_REJECT_REQUEST = -1;
/**
* Get Filters using provided NiFi Properties and append filters for Access Token Requests
*
* @param properties NiFi Properties required
* @return List of Filter Holders
*/
@Override
public List<FilterHolder> getFilters(final NiFiProperties properties) {
final List<FilterHolder> filters = super.getFilters(properties);
final FilterHolder accessTokenDenialOfServiceFilter = getAccessTokenDenialOfServiceFilter(properties);
filters.add(accessTokenDenialOfServiceFilter);
return filters;
}
private FilterHolder getAccessTokenDenialOfServiceFilter(final NiFiProperties properties) {
final FilterHolder filter = getDenialOfServiceFilter(properties, DoSFilter.class);
final int maxWebAccessTokenRequestsPerSecond = properties.getMaxWebAccessTokenRequestsPerSecond();
filter.setInitParameter("maxRequestsPerSec", Integer.toString(maxWebAccessTokenRequestsPerSecond));
filter.setInitParameter("maxWaitMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
filter.setInitParameter("delayMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
filter.setInitParameter(FilterParameter.PATH_SPECIFICATION.name(), RELATIVE_PATH_ACCESS_TOKEN);
filter.setName("AccessToken-DoSFilter");
return filter;
}
}

View File

@ -0,0 +1,127 @@
/*
* 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.filter;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.processor.DataUnit;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.apache.nifi.web.security.requests.ContentLengthFilter;
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlets.DoSFilter;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Standard implementation of Request Filter Provider
*/
public class StandardRequestFilterProvider implements RequestFilterProvider {
private static final int MAX_CONTENT_SIZE_DISABLED = 0;
/**
* Get Filters using provided NiFi Properties
*
* @param properties NiFi Properties required
* @return List of Filter Holders
*/
@Override
public List<FilterHolder> getFilters(final NiFiProperties properties) {
Objects.requireNonNull(properties, "Properties required");
final List<FilterHolder> filters = new ArrayList<>();
filters.add(getFilterHolder(XFrameOptionsFilter.class));
filters.add(getFilterHolder(ContentSecurityPolicyFilter.class));
filters.add(getFilterHolder(XSSProtectionFilter.class));
filters.add(getFilterHolder(XContentTypeOptionsFilter.class));
if (properties.isHTTPSConfigured()) {
filters.add(getFilterHolder(StrictTransportSecurityFilter.class));
filters.add(getFilterHolder(RequestAuthenticationFilter.class));
}
final int maxContentSize = getMaxContentSize(properties);
if (maxContentSize > MAX_CONTENT_SIZE_DISABLED) {
final FilterHolder contentLengthFilter = getContentLengthFilter(maxContentSize);
filters.add(contentLengthFilter);
}
final FilterHolder denialOfServiceFilter = getDenialOfServiceFilter(properties, DataTransferExcludedDoSFilter.class);
filters.add(denialOfServiceFilter);
return filters;
}
protected FilterHolder getDenialOfServiceFilter(final NiFiProperties properties, final Class<? extends DoSFilter> filterClass) {
final FilterHolder filter = new FilterHolder(filterClass);
final int maxWebRequestsPerSecond = properties.getMaxWebRequestsPerSecond();
filter.setInitParameter("maxRequestsPerSec", Integer.toString(maxWebRequestsPerSecond));
final long webRequestTimeout = getWebRequestTimeout(properties);
filter.setInitParameter("maxRequestMs", Long.toString(webRequestTimeout));
final String webRequestIpWhitelist = properties.getWebRequestIpWhitelist();
filter.setInitParameter("ipWhitelist", webRequestIpWhitelist);
filter.setName(DoSFilter.class.getSimpleName());
return filter;
}
private FilterHolder getFilterHolder(final Class<? extends Filter> filterClass) {
final FilterHolder filter = new FilterHolder(filterClass);
filter.setName(filterClass.getSimpleName());
return filter;
}
private FilterHolder getContentLengthFilter(final int maxContentSize) {
final FilterHolder filter = new FilterHolder(ContentLengthFilter.class);
filter.setInitParameter(ContentLengthFilter.MAX_LENGTH_INIT_PARAM, Integer.toString(maxContentSize));
filter.setName(ContentLengthFilter.class.getSimpleName());
return filter;
}
private int getMaxContentSize(final NiFiProperties properties) {
final String webMaxContentSize = properties.getWebMaxContentSize();
try {
return StringUtils.isBlank(webMaxContentSize) ? MAX_CONTENT_SIZE_DISABLED : DataUnit.parseDataSize(webMaxContentSize, DataUnit.B).intValue();
} catch (final IllegalArgumentException e) {
throw new IllegalStateException(String.format("Property [%s] format invalid", NiFiProperties.WEB_MAX_CONTENT_SIZE), e);
}
}
protected long getWebRequestTimeout(final NiFiProperties properties) {
final String webRequestTimeout = properties.getWebRequestTimeout();
try {
final double webRequestTimeoutParsed = FormatUtils.getPreciseTimeDuration(webRequestTimeout, TimeUnit.MILLISECONDS);
return Math.round(webRequestTimeoutParsed);
} catch (final NumberFormatException e) {
throw new IllegalStateException(String.format("Property [%s] format invalid", NiFiProperties.WEB_REQUEST_TIMEOUT), e);
}
}
}

View File

@ -1,435 +0,0 @@
/*
* 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 org.apache.nifi.bundle.Bundle
import org.apache.nifi.nar.ExtensionManagerHolder
import org.apache.nifi.processor.DataUnit
import org.apache.nifi.remote.io.socket.NetworkUtils
import org.apache.nifi.security.util.StandardTlsConfiguration
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder
import org.apache.nifi.security.util.TlsConfiguration
import org.apache.nifi.security.util.TlsPlatform
import org.apache.nifi.util.NiFiProperties
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.eclipse.jetty.server.Connector
import org.eclipse.jetty.server.HttpConfiguration
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.SslConnectionFactory
import org.eclipse.jetty.servlet.FilterHolder
import org.eclipse.jetty.util.ssl.SslContextFactory
import org.eclipse.jetty.webapp.WebAppContext
import org.junit.After
import org.junit.Assume
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.contrib.java.lang.system.Assertion
import org.junit.contrib.java.lang.system.ExpectedSystemExit
import org.junit.contrib.java.lang.system.SystemErrRule
import org.junit.contrib.java.lang.system.SystemOutRule
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.servlet.DispatcherType
import java.nio.charset.StandardCharsets
import java.security.Security
@RunWith(JUnit4.class)
class JettyServerGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(JettyServerGroovyTest.class)
@Rule
public final ExpectedSystemExit exit = ExpectedSystemExit.none()
@Rule
public final SystemOutRule systemOutRule = new SystemOutRule().enableLog()
@Rule
public final SystemErrRule systemErrRule = new SystemErrRule().enableLog()
private static final int DEFAULT_HTTP_PORT = 8080
private static final int DEFAULT_HTTPS_PORT = 8443
private static final int HTTPS_PORT = NetworkUtils.getAvailableTcpPort()
private static final String HTTPS_HOSTNAME = "localhost"
private static final String TLS_1_3_PROTOCOL = "TLSv1.3"
private static final List<String> TLS_1_3_CIPHER_SUITES = ["TLS_AES_128_GCM_SHA256"]
private static final TlsConfiguration TLS_CONFIGURATION = new TemporaryKeyStoreBuilder().build()
// These protocol versions should not ever be supported
static private final List<String> LEGACY_TLS_PROTOCOLS = ["TLS", "TLSv1", "TLSv1.1", "SSL", "SSLv2", "SSLv2Hello", "SSLv3"]
NiFiProperties httpsProps = new NiFiProperties(new Properties([
(NiFiProperties.WEB_HTTPS_PORT) : HTTPS_PORT as String,
(NiFiProperties.WEB_HTTPS_HOST) : HTTPS_HOSTNAME,
(NiFiProperties.SECURITY_KEYSTORE) : TLS_CONFIGURATION.keystorePath,
(NiFiProperties.SECURITY_KEYSTORE_PASSWD) : TLS_CONFIGURATION.keystorePassword,
(NiFiProperties.SECURITY_KEYSTORE_TYPE) : TLS_CONFIGURATION.keystoreType.type,
(NiFiProperties.SECURITY_TRUSTSTORE) : TLS_CONFIGURATION.truststorePath,
(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD): TLS_CONFIGURATION.truststorePassword,
(NiFiProperties.SECURITY_TRUSTSTORE_TYPE) : TLS_CONFIGURATION.truststoreType.type,
]))
@BeforeClass
static void setUpOnce() throws Exception {
new File(TLS_CONFIGURATION.keystorePath).deleteOnExit()
new File(TLS_CONFIGURATION.truststorePath).deleteOnExit()
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@After
void tearDown() throws Exception {
// Cleans up the EMH so it can be reinitialized when a new Jetty server starts
ExtensionManagerHolder.INSTANCE = null
}
@Test
void testShouldDetectHttpAndHttpsConfigurationsBothPresent() {
// Arrange
Map badProps = [
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
(NiFiProperties.WEB_THREADS) : NiFiProperties.DEFAULT_WEB_THREADS
]
NiFiProperties mockProps = Mockito.mock(NiFiProperties.class)
Mockito.when(mockProps.getPort()).thenReturn(DEFAULT_HTTP_PORT)
Mockito.when(mockProps.getSslPort()).thenReturn(DEFAULT_HTTPS_PORT)
Mockito.when(mockProps.getProperty(ArgumentMatchers.anyString())).thenAnswer(new Answer<Object>() {
@Override
Object answer(InvocationOnMock invocation) throws Throwable {
badProps[(String) invocation.getArgument(0)] ?: "no_value"
}
})
// Act
boolean bothConfigsPresent = JettyServer.bothHttpAndHttpsConnectorsConfigured(mockProps)
logger.info("Both configs present: ${bothConfigsPresent}")
// Assert
assert bothConfigsPresent
}
@Test
void testDetectHttpAndHttpsConfigurationsShouldAllowEither() {
// Arrange
Map httpMap = [
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
(NiFiProperties.WEB_HTTPS_HOST): null,
]
NiFiProperties httpProps = [
getPort : { -> DEFAULT_HTTP_PORT },
getSslPort : { -> null },
getProperty: { String prop ->
String value = httpMap[prop] ?: "no_value"
value
},
] as NiFiProperties
Map httpsMap = [
(NiFiProperties.WEB_HTTP_HOST) : null,
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
]
NiFiProperties httpsProps = [
getPort : { -> null },
getSslPort : { -> DEFAULT_HTTPS_PORT },
getProperty: { String prop ->
String value = httpsMap[prop] ?: "no_value"
value
},
] as NiFiProperties
// Act
boolean bothConfigsPresentForHttp = JettyServer.bothHttpAndHttpsConnectorsConfigured(httpProps)
logger.info("Both configs present for HTTP properties: ${bothConfigsPresentForHttp}")
boolean bothConfigsPresentForHttps = JettyServer.bothHttpAndHttpsConnectorsConfigured(httpsProps)
logger.info("Both configs present for HTTPS properties: ${bothConfigsPresentForHttps}")
// Assert
assert !bothConfigsPresentForHttp
assert !bothConfigsPresentForHttps
}
@Test
void testShouldFailToStartWithHttpAndHttpsConfigurationsBothPresent() {
// Arrange
Map badProps = [
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
]
NiFiProperties mockProps = [
getPort : { -> DEFAULT_HTTP_PORT },
getSslPort : { -> DEFAULT_HTTPS_PORT },
getProperty : { String prop ->
String value = badProps[prop] ?: "no_value"
logger.mock("getProperty(${prop}) -> ${value}")
value
},
getWebThreads : { -> NiFiProperties.DEFAULT_WEB_THREADS },
getWebMaxHeaderSize: { -> NiFiProperties.DEFAULT_WEB_MAX_HEADER_SIZE },
isHTTPSConfigured : { -> true }
] as NiFiProperties
// The web server should fail to start and exit Java
exit.expectSystemExitWithStatus(1)
exit.checkAssertionAfterwards(new Assertion() {
void checkAssertion() {
final String standardErr = systemErrRule.getLog()
List<String> errLines = standardErr.split("\n")
assert errLines.any { it =~ "Failed to start web server: " }
assert errLines.any { it =~ "Shutting down..." }
}
})
// Act
JettyServer jettyServer = new JettyServer()
jettyServer.initialize(mockProps, null, [] as Set<Bundle>, null)
// Assert
// Assertions defined above
}
@Test
void testShouldConfigureHTTPSConnector() {
// Arrange
final String externalHostname = "localhost"
NiFiProperties httpsProps = new NiFiProperties(new Properties([
(NiFiProperties.WEB_HTTPS_PORT): HTTPS_PORT as String,
(NiFiProperties.WEB_HTTPS_HOST): externalHostname,
]))
Server internalServer = new Server()
JettyServer jetty = new JettyServer(internalServer, httpsProps)
// Act
jetty.configureHttpsConnector(internalServer, new HttpConfiguration())
List<Connector> connectors = Arrays.asList(internalServer.connectors)
// Assert
assertServerConnector(connectors, externalHostname, HTTPS_PORT)
}
@Test
void testShouldSupportTLSv1_3WhenProtocolFound() {
// Arrange
String[] defaultProtocols = SSLContext.getDefault().defaultSSLParameters.protocols
Assume.assumeTrue("This test should only run when TLSv1.3 is found in the set of default protocols", defaultProtocols.contains(TLS_1_3_PROTOCOL))
Server internalServer = new Server()
JettyServer jetty = new JettyServer(internalServer, httpsProps)
jetty.configureConnectors(internalServer)
List<Connector> connectors = Arrays.asList(internalServer.connectors)
internalServer.start()
// Create a (client) socket which only supports TLSv1.3
TlsConfiguration tls13ClientConf = StandardTlsConfiguration.fromNiFiProperties(httpsProps)
SSLSocketFactory socketFactory = org.apache.nifi.security.util.SslContextFactory.createSSLSocketFactory(tls13ClientConf)
SSLSocket socket = (SSLSocket) socketFactory.createSocket(HTTPS_HOSTNAME, HTTPS_PORT)
socket.setEnabledProtocols([TLS_1_3_PROTOCOL] as String[])
socket.setEnabledCipherSuites(TLS_1_3_CIPHER_SUITES as String[])
// Act
String response = makeTLSRequest(socket, "This is a TLS 1.3 request")
// Assert
assert response =~ "HTTP/1.1 400"
assertServerConnector(connectors)
// Clean up
internalServer.stop()
}
@Test
void testShouldNotSupportTLSv1_3WhenProtocolNotFound() {
// Arrange
Assume.assumeTrue("This test should only run when TLSv1.3 is not found in the set of default protocols", !TlsPlatform.supportedProtocols.contains(TLS_1_3_PROTOCOL))
Server internalServer = new Server()
JettyServer jetty = new JettyServer(internalServer, httpsProps)
jetty.configureConnectors(internalServer)
List<Connector> connectors = Arrays.asList(internalServer.connectors)
internalServer.start()
TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(httpsProps)
// Create a "default" (client) socket (which supports TLSv1.2)
SSLSocketFactory defaultSocketFactory = org.apache.nifi.security.util.SslContextFactory.createSSLSocketFactory(tlsConfiguration)
SSLSocket defaultSocket = (SSLSocket) defaultSocketFactory.createSocket(HTTPS_HOSTNAME, HTTPS_PORT)
// Act
String tls12Response = makeTLSRequest(defaultSocket, "This is a default socket request")
def msg = shouldFail() {
// Create a (client) socket which only supports TLSv1.3
SSLSocketFactory tls13SocketFactory = org.apache.nifi.security.util.SslContextFactory.createSSLSocketFactory(tlsConfiguration)
SSLSocket tls13Socket = (SSLSocket) tls13SocketFactory.createSocket(HTTPS_HOSTNAME, HTTPS_PORT)
tls13Socket.setEnabledProtocols([TLS_1_3_PROTOCOL] as String[])
tls13Socket.setEnabledCipherSuites(TLS_1_3_CIPHER_SUITES as String[])
makeTLSRequest(tls13Socket, "This is a TLSv1.3 socket request")
}
logger.expected(msg)
// Assert
assert tls12Response =~ "HTTP"
assertServerConnector(connectors)
// Clean up
internalServer.stop()
}
/**
* Returns the server's response body as a String. Closes the socket connection.
*
* @param socket
* @param requestMessage
* @return
*/
private static String makeTLSRequest(Socket socket, String requestMessage) {
InputStream socketInputStream = new BufferedInputStream(socket.getInputStream())
OutputStream socketOutputStream = new BufferedOutputStream(socket.getOutputStream())
socketOutputStream.write(requestMessage.getBytes())
socketOutputStream.flush()
byte[] data = new byte[2048]
int len = socketInputStream.read(data)
if (len <= 0) {
throw new IOException("no data received")
}
final String trimmedResponse = new String(data, 0, len, StandardCharsets.UTF_8)
logger.info("Client received ${len} bytes from server: \n${trimmedResponse}\n----End of response----")
socket.close()
trimmedResponse
}
private static void assertServerConnector(List<Connector> connectors,
String EXPECTED_HOSTNAME = HTTPS_HOSTNAME,
int EXPECTED_PORT = HTTPS_PORT) {
// Assert the server connector is correct
assert connectors.size() == 1
ServerConnector connector = connectors.first() as ServerConnector
assert connector.host == EXPECTED_HOSTNAME
assert connector.port == EXPECTED_PORT
assert connector.getProtocols() == ['ssl', 'http/1.1']
SslConnectionFactory connectionFactory = connector.getConnectionFactory("ssl") as SslConnectionFactory
SslContextFactory sslContextFactory = connectionFactory.getSslContextFactory()
assert (sslContextFactory.getExcludeProtocols() as List<String>).containsAll(LEGACY_TLS_PROTOCOLS)
}
@Test
void testShouldEnableContentLengthFilterIfWebMaxContentSizeSet() {
// Arrange
Map defaultProps = [
(NiFiProperties.WEB_HTTP_PORT) : DEFAULT_HTTP_PORT as String,
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
(NiFiProperties.WEB_MAX_CONTENT_SIZE): "1 MB",
]
NiFiProperties mockProps = new NiFiProperties(new Properties(defaultProps))
List<FilterHolder> filters = []
def mockWebContext = [
addFilter: { FilterHolder fh, String path, EnumSet<DispatcherType> d ->
logger.mock("Called addFilter(${fh.name}, ${path}, ${d})")
filters.add(fh)
fh
}] as WebAppContext
JettyServer jettyServer = new JettyServer(new Server(), mockProps)
logger.info("Created JettyServer: ${jettyServer.dump()}")
final int MAX_CONTENT_LENGTH_BYTES = DataUnit.parseDataSize(defaultProps[NiFiProperties.WEB_MAX_CONTENT_SIZE], DataUnit.B).intValue()
// Act
jettyServer.addDenialOfServiceFilters(mockWebContext, mockProps)
// Assert
assert filters.size() == 2
def filterNames = filters*.name
logger.info("Web API Context has ${filters.size()} filters: ${filterNames.join(", ")}".toString())
assert filterNames.contains("DoSFilter")
assert filterNames.contains("ContentLengthFilter")
FilterHolder clfHolder = filters.find { it.name == "ContentLengthFilter" }
String maxContentLength = clfHolder.getInitParameter("maxContentLength")
assert maxContentLength == MAX_CONTENT_LENGTH_BYTES as String
// Filter is not instantiated just by adding it
// ContentLengthFilter clf = filters?.find { it.className == "ContentLengthFilter" }?.filter as ContentLengthFilter
// assert clf.getMaxContentLength() == MAX_CONTENT_LENGTH_BYTES
}
@Test
void testShouldNotEnableContentLengthFilterIfWebMaxContentSizeEmpty() {
// Arrange
Map defaultProps = [
(NiFiProperties.WEB_HTTP_PORT): DEFAULT_HTTP_PORT as String,
(NiFiProperties.WEB_HTTP_HOST): "localhost",
]
NiFiProperties mockProps = new NiFiProperties(new Properties(defaultProps))
List<FilterHolder> filters = []
def mockWebContext = [
addFilter: { FilterHolder fh, String path, EnumSet<DispatcherType> d ->
logger.mock("Called addFilter(${fh.name}, ${path}, ${d})")
filters.add(fh)
fh
}] as WebAppContext
JettyServer jettyServer = new JettyServer(new Server(), mockProps)
logger.info("Created JettyServer: ${jettyServer.dump()}")
// Act
jettyServer.addDenialOfServiceFilters(mockWebContext, mockProps)
// Assert
assert filters.size() == 1
def filterNames = filters*.name
logger.info("Web API Context has ${filters.size()} filters: ${filterNames.join(", ")}".toString())
assert filterNames.contains("DoSFilter")
assert !filterNames.contains("ContentLengthFilter")
}
}

View File

@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.server.filter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DataTransferExcludedDoSFilterTest {
private static final String DATA_TRANSFER_URI = "/nifi-api/data-transfer";
private static final String ACCESS_URI = "/nifi-api/access";
@Mock
private FilterConfig filterConfig;
@Mock
private FilterChain filterChain;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
private DataTransferExcludedDoSFilter filter;
@BeforeEach
public void setFilter() throws ServletException {
filter = new DataTransferExcludedDoSFilter();
filter.init(filterConfig);
}
@Test
public void testDoFilterChain() throws ServletException, IOException {
when(request.getRequestURI()).thenReturn(ACCESS_URI);
filter.doFilterChain(filterChain, request, response);
verify(request, never()).setAttribute(eq(DataTransferExcludedDoSFilter.DATA_TRANSFER_URI_ATTRIBUTE), eq(DATA_TRANSFER_URI));
}
@Test
public void testDoFilterChainDataTransfer() throws ServletException, IOException {
when(request.getRequestURI()).thenReturn(DATA_TRANSFER_URI);
filter.doFilterChain(filterChain, request, response);
verify(request).setAttribute(eq(DataTransferExcludedDoSFilter.DATA_TRANSFER_URI_ATTRIBUTE), eq(DATA_TRANSFER_URI));
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.filter;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlets.DoSFilter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.servlet.Filter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RestApiRequestFilterProviderTest {
private RestApiRequestFilterProvider provider;
@BeforeEach
public void setProvider() {
provider = new RestApiRequestFilterProvider();
}
@Test
public void testGetFilters() {
final NiFiProperties properties = getProperties(Collections.emptyMap());
final List<FilterHolder> filters = provider.getFilters(properties);
final Optional<FilterHolder> filterHolder = filters.stream().filter(filter -> filter.getInitParameter(FilterParameter.PATH_SPECIFICATION.name()) != null).findFirst();
assertTrue(filterHolder.isPresent());
final FilterHolder holder = filterHolder.get();
assertEquals(DoSFilter.class, holder.getHeldClass());
assertNotNull(filters);
assertFalse(filters.isEmpty());
assertFilterClassFound(filters, DataTransferExcludedDoSFilter.class);
assertFilterClassFound(filters, XFrameOptionsFilter.class);
assertFilterClassFound(filters, ContentSecurityPolicyFilter.class);
assertFilterClassFound(filters, XSSProtectionFilter.class);
assertFilterClassFound(filters, XContentTypeOptionsFilter.class);
}
private void assertFilterClassFound(final List<FilterHolder> filters, final Class<? extends Filter> filterClass) {
final Optional<FilterHolder> filterHolder = filters.stream().filter(filter -> filterClass.equals(filter.getHeldClass())).findFirst();
assertTrue(filterHolder.isPresent(), String.format("Filter Class [%s] not found", filterClass));
}
private NiFiProperties getProperties(final Map<String, String> properties) {
return NiFiProperties.createBasicNiFiProperties(null, properties);
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.filter;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.apache.nifi.web.security.requests.ContentLengthFilter;
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
import org.eclipse.jetty.servlet.FilterHolder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.servlet.Filter;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class StandardRequestFilterProviderTest {
private static final String HTTPS_PORT = "8443";
private static final String MAX_CONTENT_SIZE = "1 MB";
private StandardRequestFilterProvider provider;
@BeforeEach
public void setProvider() {
provider = new StandardRequestFilterProvider();
}
@Test
public void testGetFilters() {
final NiFiProperties properties = getProperties(Collections.emptyMap());
final List<FilterHolder> filters = provider.getFilters(properties);
assertStandardFiltersFound(filters);
}
@Test
public void testGetFiltersContentLengthEnabled() {
final Map<String, String> configurationProperties = new LinkedHashMap<>();
configurationProperties.put(NiFiProperties.WEB_MAX_CONTENT_SIZE, MAX_CONTENT_SIZE);
final NiFiProperties properties = getProperties(configurationProperties);
final List<FilterHolder> filters = provider.getFilters(properties);
assertStandardFiltersFound(filters);
assertFilterClassFound(filters, ContentLengthFilter.class);
}
@Test
public void testGetFiltersHttpsEnabled() {
final Map<String, String> configurationProperties = new LinkedHashMap<>();
configurationProperties.put(NiFiProperties.WEB_HTTPS_PORT, HTTPS_PORT);
final NiFiProperties properties = getProperties(configurationProperties);
final List<FilterHolder> filters = provider.getFilters(properties);
assertStandardFiltersFound(filters);
assertFilterClassFound(filters, RequestAuthenticationFilter.class);
assertFilterClassFound(filters, StrictTransportSecurityFilter.class);
}
private void assertStandardFiltersFound(final List<FilterHolder> filters) {
assertNotNull(filters);
assertFalse(filters.isEmpty());
assertFilterClassFound(filters, DataTransferExcludedDoSFilter.class);
assertFilterClassFound(filters, XFrameOptionsFilter.class);
assertFilterClassFound(filters, ContentSecurityPolicyFilter.class);
assertFilterClassFound(filters, XSSProtectionFilter.class);
assertFilterClassFound(filters, XContentTypeOptionsFilter.class);
}
private void assertFilterClassFound(final List<FilterHolder> filters, final Class<? extends Filter> filterClass) {
final Optional<FilterHolder> filterHolder = filters.stream().filter(filter -> filterClass.equals(filter.getHeldClass())).findFirst();
assertTrue(filterHolder.isPresent(), String.format("Filter Class [%s] not found", filterClass));
}
private NiFiProperties getProperties(final Map<String, String> properties) {
return NiFiProperties.createBasicNiFiProperties(null, properties);
}
}