From fc27b3138bba69c52d3c7e4158c77a265410981f Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Tue, 18 Jan 2022 17:19:06 -0600 Subject: [PATCH] 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 This closes #5670. --- .../apache/nifi/web/server/JettyServer.java | 165 +------ .../filter/DataTransferExcludedDoSFilter.java | 54 +++ .../web/server/filter/FilterParameter.java | 24 + .../server/filter/RequestFilterProvider.java | 35 ++ .../filter/RestApiRequestFilterProvider.java | 62 +++ .../filter/StandardRequestFilterProvider.java | 127 +++++ .../web/server/JettyServerGroovyTest.groovy | 435 ------------------ .../DataTransferExcludedDoSFilterTest.java | 80 ++++ .../RestApiRequestFilterProviderTest.java | 77 ++++ .../StandardRequestFilterProviderTest.java | 108 +++++ 10 files changed, 582 insertions(+), 585 deletions(-) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilter.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/FilterParameter.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RequestFilterProvider.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProvider.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/StandardRequestFilterProvider.java delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilterTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProviderTest.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/StandardRequestFilterProviderTest.java diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java index d9aefa3ef8..b5d5b7ac78 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java @@ -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 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> filters = - new ArrayList<>(Arrays.asList( - XFrameOptionsFilter.class, - ContentSecurityPolicyFilter.class, - XSSProtectionFilter.class, - XContentTypeOptionsFilter.class)); + final List 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 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() {{ - 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() {{ - 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() {{ - 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. *

diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilter.java new file mode 100644 index 0000000000..83629dc113 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilter.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.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); + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/FilterParameter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/FilterParameter.java new file mode 100644 index 0000000000..8b773d57e8 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/FilterParameter.java @@ -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 +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RequestFilterProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RequestFilterProvider.java new file mode 100644 index 0000000000..45519975b5 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RequestFilterProvider.java @@ -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 getFilters(NiFiProperties properties); +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProvider.java new file mode 100644 index 0000000000..ee114fc4aa --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProvider.java @@ -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 getFilters(final NiFiProperties properties) { + final List 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; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/StandardRequestFilterProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/StandardRequestFilterProvider.java new file mode 100644 index 0000000000..eab2f70815 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/filter/StandardRequestFilterProvider.java @@ -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 getFilters(final NiFiProperties properties) { + Objects.requireNonNull(properties, "Properties required"); + + final List 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 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 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); + } + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy deleted file mode 100644 index e7804d7e70..0000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy +++ /dev/null @@ -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 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 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() { - @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 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, 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 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 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 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 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).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 filters = [] - def mockWebContext = [ - addFilter: { FilterHolder fh, String path, EnumSet 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 filters = [] - def mockWebContext = [ - addFilter: { FilterHolder fh, String path, EnumSet 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") - } -} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilterTest.java new file mode 100644 index 0000000000..5017133179 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/DataTransferExcludedDoSFilterTest.java @@ -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)); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProviderTest.java new file mode 100644 index 0000000000..b00f308170 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/RestApiRequestFilterProviderTest.java @@ -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 filters = provider.getFilters(properties); + + final Optional 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 filters, final Class filterClass) { + final Optional 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 properties) { + return NiFiProperties.createBasicNiFiProperties(null, properties); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/StandardRequestFilterProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/StandardRequestFilterProviderTest.java new file mode 100644 index 0000000000..feb4ba0148 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/filter/StandardRequestFilterProviderTest.java @@ -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 filters = provider.getFilters(properties); + + assertStandardFiltersFound(filters); + } + + @Test + public void testGetFiltersContentLengthEnabled() { + final Map configurationProperties = new LinkedHashMap<>(); + configurationProperties.put(NiFiProperties.WEB_MAX_CONTENT_SIZE, MAX_CONTENT_SIZE); + + final NiFiProperties properties = getProperties(configurationProperties); + final List filters = provider.getFilters(properties); + + assertStandardFiltersFound(filters); + + assertFilterClassFound(filters, ContentLengthFilter.class); + } + + @Test + public void testGetFiltersHttpsEnabled() { + final Map configurationProperties = new LinkedHashMap<>(); + configurationProperties.put(NiFiProperties.WEB_HTTPS_PORT, HTTPS_PORT); + + final NiFiProperties properties = getProperties(configurationProperties); + final List filters = provider.getFilters(properties); + + assertStandardFiltersFound(filters); + + assertFilterClassFound(filters, RequestAuthenticationFilter.class); + assertFilterClassFound(filters, StrictTransportSecurityFilter.class); + } + + private void assertStandardFiltersFound(final List 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 filters, final Class filterClass) { + final Optional 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 properties) { + return NiFiProperties.createBasicNiFiProperties(null, properties); + } +}